From 3e91f535d9edb0316f14afc4121fbea41e94a1ba Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Wed, 22 May 2024 16:23:50 -0700 Subject: [PATCH 01/97] Refresh note when changed in database Related to https://github.com/laurent22/joplin/pull/10459, but needs more testing -- this change could cause regressions. --- .../gui/NoteEditor/utils/useFormNote.test.ts | 78 ++++++++++--------- .../gui/NoteEditor/utils/useFormNote.ts | 18 +++++ 2 files changed, 59 insertions(+), 37 deletions(-) diff --git a/packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.ts b/packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.ts index 345c1015258..c6f083345c0 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.ts @@ -1,6 +1,6 @@ import Note from '@joplin/lib/models/Note'; import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils'; -import { renderHook } from '@testing-library/react-hooks'; +import { act, renderHook } from '@testing-library/react-hooks'; import useFormNote, { HookDependencies } from './useFormNote'; const defaultFormNoteProps: HookDependencies = { @@ -42,28 +42,34 @@ describe('useFormNote', () => { }); }); - await Note.save({ - id: testNote.id, - encryption_cipher_text: 'cipher_text', - encryption_applied: 1, + await act(async () => { + await Note.save({ + id: testNote.id, + encryption_cipher_text: 'cipher_text', + encryption_applied: 1, + }); }); // Sync starting should cause a re-render formNote.rerender(makeFormNoteProps(false, false)); - await formNote.waitFor(() => { - expect(formNote.result.current.formNote).toMatchObject({ - encryption_applied: 1, + await act(async () => { + await formNote.waitFor(() => { + expect(formNote.result.current.formNote).toMatchObject({ + encryption_applied: 1, + }); }); }); formNote.rerender(makeFormNoteProps(false, true)); - await Note.save({ - id: testNote.id, - encryption_applied: 0, - title: 'Test Note!', + await act(async () => { + await Note.save({ + id: testNote.id, + encryption_applied: 0, + title: 'Test Note!', + }); }); // Ending decryption should also cause a re-render @@ -75,38 +81,36 @@ describe('useFormNote', () => { title: 'Test Note!', }); }); - }); - // It seems this test is crashing the worker on CI (out of memory), so disabling it for now. + formNote.unmount(); + }); - // it('should reload the note when it is changed outside of the editor', async () => { - // const note = await Note.save({ title: 'Test Note!' }); + it('should reload the note when it is changed outside of the editor', async () => { + const note = await Note.save({ title: 'Test Note!', body: '...' }); - // const makeFormNoteProps = (dbNote: DbNote): HookDependencies => { - // return { - // ...defaultFormNoteProps, - // noteId: note.id, - // dbNote, - // }; - // }; + const props = { + ...defaultFormNoteProps, + noteId: note.id, + }; - // const formNote = renderHook(props => useFormNote(props), { - // initialProps: makeFormNoteProps({ id: note.id, updated_time: note.updated_time }), - // }); + const formNote = renderHook(props => useFormNote(props), { + initialProps: props, + }); - // await formNote.waitFor(() => { - // expect(formNote.result.current.formNote.title).toBe('Test Note!'); - // }); + await formNote.waitFor(() => { + expect(formNote.result.current.formNote.title).toBe('Test Note!'); + }); - // // Simulate the note being modified outside the editor - // const modifiedNote = await Note.save({ id: note.id, title: 'Modified' }); + // Simulate the note being modified outside the editor + await act(async () => { + await Note.save({ id: note.id, title: 'Modified' }); + }); - // // NoteEditor then would update `dbNote` - // formNote.rerender(makeFormNoteProps({ id: note.id, updated_time: modifiedNote.updated_time })); + await formNote.waitFor(() => { + expect(formNote.result.current.formNote.title).toBe('Modified'); + }); - // await formNote.waitFor(() => { - // expect(formNote.result.current.formNote.title).toBe('Modified'); - // }); - // }); + formNote.unmount(); + }); }); diff --git a/packages/app-desktop/gui/NoteEditor/utils/useFormNote.ts b/packages/app-desktop/gui/NoteEditor/utils/useFormNote.ts index 0d8d61e33e3..68a1dea069c 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/useFormNote.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/useFormNote.ts @@ -15,6 +15,7 @@ import DecryptionWorker from '@joplin/lib/services/DecryptionWorker'; import { NoteEntity } from '@joplin/lib/services/database/types'; import { focus } from '@joplin/lib/utils/focusHandler'; import Logger from '@joplin/utils/Logger'; +import eventManager, { EventName } from '@joplin/lib/eventManager'; const logger = Logger.create('useFormNote'); @@ -202,6 +203,23 @@ export default function useFormNote(dependencies: HookDependencies) { refreshFormNote, ]); + useEffect(() => { + if (!noteId) return ()=>{}; + + type ChangeEventSlice = { itemId: string }; + const listener = ({ itemId }: ChangeEventSlice) => { + if (itemId === noteId) { + if (formNoteRef.current.hasChanged) return; + refreshFormNote(); + } + }; + eventManager.on(EventName.ItemChange, listener); + + return () => { + eventManager.off(EventName.ItemChange, listener); + }; + }, [noteId, refreshFormNote]); + useEffect(() => { if (!noteId) { if (formNote.id) setFormNote(defaultFormNote()); From e7294f031963daa94dcf83e0f40d783bc8391f8d Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 4 Oct 2024 22:31:56 -0700 Subject: [PATCH 02/97] Desktop: PoC: Show the note editor in a new window --- .eslintignore | 2 + .gitignore | 2 + packages/app-desktop/ElectronAppWrapper.ts | 12 +++ .../gui/EditFolderDialog/IconSelector.tsx | 4 +- .../app-desktop/gui/MainScreen/MainScreen.tsx | 4 +- .../v6/utils/useRefocusOnVisiblePaneChange.ts | 4 +- .../NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx | 43 +++++---- .../NoteBody/TinyMCE/utils/useWebViewApi.ts | 9 +- .../gui/NoteEditor/NoteEditorWrapper.tsx | 87 +++++++++++++++++++ .../gui/NoteEditor/NoteTitle/NoteTitleBar.tsx | 32 +------ .../app-desktop/gui/NoteEditor/style.scss | 2 + .../styles/note-title-info-group.scss | 11 +++ .../NoteEditor/styles/note-title-wrapper.scss | 12 +++ packages/app-desktop/gui/NoteTextViewer.tsx | 10 ++- .../gui/StyleSheets/StyleSheetContainer.tsx | 15 ++-- packages/app-desktop/gui/ToolbarBase.tsx | 10 ++- packages/app-desktop/gui/hooks/useDom.ts | 9 ++ packages/app-desktop/gui/utils/loadScript.ts | 2 +- packages/app-desktop/index.html | 31 ------- packages/app-desktop/main.scss | 32 +++++++ 20 files changed, 237 insertions(+), 96 deletions(-) create mode 100644 packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx create mode 100644 packages/app-desktop/gui/NoteEditor/styles/note-title-info-group.scss create mode 100644 packages/app-desktop/gui/NoteEditor/styles/note-title-wrapper.scss create mode 100644 packages/app-desktop/gui/hooks/useDom.ts diff --git a/.eslintignore b/.eslintignore index 2ef2e7897e2..42526afaba2 100644 --- a/.eslintignore +++ b/.eslintignore @@ -302,6 +302,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTabIndenter.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js packages/app-desktop/gui/NoteEditor/NoteEditor.js +packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.js packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js packages/app-desktop/gui/NoteEditor/WarningBanner/BannerContent.js packages/app-desktop/gui/NoteEditor/WarningBanner/WarningBanner.js @@ -446,6 +447,7 @@ packages/app-desktop/gui/ToolbarSpace.js packages/app-desktop/gui/TrashNotification/TrashNotification.js packages/app-desktop/gui/UpdateNotification/UpdateNotification.js packages/app-desktop/gui/dialogs.js +packages/app-desktop/gui/hooks/useDom.js packages/app-desktop/gui/hooks/useEffectDebugger.js packages/app-desktop/gui/hooks/useElementHeight.js packages/app-desktop/gui/hooks/useImperativeHandlerDebugger.js diff --git a/.gitignore b/.gitignore index e452f6c36ad..3526f5b33c5 100644 --- a/.gitignore +++ b/.gitignore @@ -279,6 +279,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTabIndenter.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js packages/app-desktop/gui/NoteEditor/NoteEditor.js +packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.js packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js packages/app-desktop/gui/NoteEditor/WarningBanner/BannerContent.js packages/app-desktop/gui/NoteEditor/WarningBanner/WarningBanner.js @@ -423,6 +424,7 @@ packages/app-desktop/gui/ToolbarSpace.js packages/app-desktop/gui/TrashNotification/TrashNotification.js packages/app-desktop/gui/UpdateNotification/UpdateNotification.js packages/app-desktop/gui/dialogs.js +packages/app-desktop/gui/hooks/useDom.js packages/app-desktop/gui/hooks/useEffectDebugger.js packages/app-desktop/gui/hooks/useElementHeight.js packages/app-desktop/gui/hooks/useImperativeHandlerDebugger.js diff --git a/packages/app-desktop/ElectronAppWrapper.ts b/packages/app-desktop/ElectronAppWrapper.ts index ba271a401b1..dc00124674d 100644 --- a/packages/app-desktop/ElectronAppWrapper.ts +++ b/packages/app-desktop/ElectronAppWrapper.ts @@ -241,6 +241,18 @@ export default class ElectronAppWrapper { } }); + // Override calls to window.open and links with target="_blank": Open most in a browser instead + // of Electron: + this.win_.webContents.setWindowOpenHandler((event) => { + if (event.url === 'about:blank') { + // Script-controlled pages: Used for opening notes in new windows + return { action: 'allow', outlivesOpener: true }; + } else if (event.url.match(/^https?:\/\//)) { + void bridge().openExternal(event.url); + } + return { action: 'deny' }; + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied this.win_.on('close', (event: any) => { // If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true) diff --git a/packages/app-desktop/gui/EditFolderDialog/IconSelector.tsx b/packages/app-desktop/gui/EditFolderDialog/IconSelector.tsx index 69f3089d238..04fc758842b 100644 --- a/packages/app-desktop/gui/EditFolderDialog/IconSelector.tsx +++ b/packages/app-desktop/gui/EditFolderDialog/IconSelector.tsx @@ -35,7 +35,7 @@ export const IconSelector = (props: Props) => { attrs: { type: 'module', }, - }); + }, document); if (event.cancelled) return; @@ -45,7 +45,7 @@ export const IconSelector = (props: Props) => { attrs: { type: 'module', }, - }); + }, document); if (event.cancelled) return; diff --git a/packages/app-desktop/gui/MainScreen/MainScreen.tsx b/packages/app-desktop/gui/MainScreen/MainScreen.tsx index 521d93f63ea..bb509716056 100644 --- a/packages/app-desktop/gui/MainScreen/MainScreen.tsx +++ b/packages/app-desktop/gui/MainScreen/MainScreen.tsx @@ -4,7 +4,6 @@ import findItemByKey from '../ResizableLayout/utils/findItemByKey'; import { MoveButtonClickEvent } from '../ResizableLayout/MoveButtons'; import { move } from '../ResizableLayout/utils/movements'; import { LayoutItem } from '../ResizableLayout/utils/types'; -import NoteEditor from '../NoteEditor/NoteEditor'; import NoteContentPropertiesDialog from '../NoteContentPropertiesDialog'; import ShareNoteDialog from '../ShareNoteDialog'; import CommandService from '@joplin/lib/services/CommandService'; @@ -49,6 +48,7 @@ import { NoteListColumns } from '@joplin/lib/services/plugins/api/noteListType'; import validateColumns from '../NoteListHeader/utils/validateColumns'; import TrashNotification from '../TrashNotification/TrashNotification'; import UpdateNotification from '../UpdateNotification/UpdateNotification'; +import NoteEditorWrapper from '../NoteEditor/NoteEditorWrapper'; const PluginManager = require('@joplin/lib/services/PluginManager'); const ipcRenderer = require('electron').ipcRenderer; @@ -791,7 +791,7 @@ class MainScreenComponent extends React.Component { } else if (this.props.settingEditorCodeView && this.props.enableLegacyMarkdownEditor) { bodyEditor = 'CodeMirror5'; } - return ; + return ; }, }; diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVisiblePaneChange.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVisiblePaneChange.ts index 1345bb5db44..db3b35d3f30 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVisiblePaneChange.ts +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVisiblePaneChange.ts @@ -12,7 +12,9 @@ interface Props { const useRefocusOnVisiblePaneChange = ({ editorRef, webviewRef, visiblePanes }: Props) => { const lastVisiblePanes = useRef(visiblePanes); useEffect(() => { - const editorHasFocus = editorRef.current?.cm6?.dom?.contains(document.activeElement); + const cm6Dom = editorRef.current?.cm6?.dom; + const doc = cm6Dom?.getRootNode() as Document|null; + const editorHasFocus = cm6Dom?.contains(doc?.activeElement); const viewerHasFocus = webviewRef.current?.hasFocus(); const lastHadViewer = lastVisiblePanes.current.includes('viewer'); diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx index 5b6f43e8481..a940aad7368 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react'; +import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle, useMemo } from 'react'; import { ScrollOptions, ScrollOptionTypes, EditorCommand, NoteBodyEditorProps, ResourceInfos, HtmlToMarkdownHandler } from '../../utils/types'; import { resourcesStatus, commandAttachFileToBody, getResourcesFromPasteEvent, processPastedHtml, attachedResources } from '../../utils/resourceHandling'; import useScroll from './utils/useScroll'; @@ -40,6 +40,7 @@ const supportedLocales = require('./supportedLocales'); import { hasProtocol } from '@joplin/utils/url'; import useTabIndenter from './utils/useTabIndenter'; import useKeyboardRefocusHandler from './utils/useKeyboardRefocusHandler'; +import useDom from '../../../hooks/useDom'; const logger = Logger.create('TinyMCE'); @@ -98,6 +99,8 @@ let changeId_ = 1; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { + const [editorContainer, setEditorContainer] = useState(null); + const editorContainerDom = useDom(editorContainer); const [editor, setEditor] = useState(null); const [scriptLoaded, setScriptLoaded] = useState(false); const [editorReady, setEditorReady] = useState(false); @@ -118,9 +121,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { contentKey: null, }); - const rootIdRef = useRef(`tinymce-${Date.now()}${Math.round(Math.random() * 10000)}`); - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - const editorRef = useRef(null); + const editorRef = useRef(null); editorRef.current = editor; const styles = styles_(props); @@ -332,6 +333,8 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { // }; useEffect(() => { + if (!editorContainerDom) return () => {}; + let cancelled = false; async function loadScripts() { @@ -350,7 +353,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { ]; for (const s of scriptsToLoad) { - if (document.getElementById(s.id)) { + if (editorContainerDom.getElementById(s.id)) { s.loaded = true; continue; } @@ -358,7 +361,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { // eslint-disable-next-line no-console console.info('Loading script', s.src); - await loadScript(s); + await loadScript(s, editorContainerDom); if (cancelled) return; s.loaded = true; @@ -372,19 +375,20 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { return () => { cancelled = true; }; - }, []); + }, [editorContainerDom]); - useWebViewApi(editor); + useWebViewApi(editor, editorContainerDom?.defaultView); const { resetModifiedTitles: resetLinkTooltips } = useLinkTooltips(editor); useEffect(() => { + if (!editorContainerDom) return () => {}; const theme = themeStyle(props.themeId); const backgroundColor = props.whiteBackgroundNoteRendering ? lightTheme.backgroundColor : theme.backgroundColor; - const element = document.createElement('style'); + const element = editorContainerDom.createElement('style'); element.setAttribute('id', 'tinyMceStyle'); - document.head.appendChild(element); - element.appendChild(document.createTextNode(` + editorContainerDom.head.appendChild(element); + element.appendChild(editorContainerDom.createTextNode(` .joplin-tinymce .tox-editor-header { padding-left: ${styles.leftExtraToolbarContainer.width + styles.leftExtraToolbarContainer.padding * 2}px; padding-right: ${styles.rightExtraToolbarContainer.width + styles.rightExtraToolbarContainer.padding * 2}px; @@ -581,7 +585,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { `)); return () => { - document.head.removeChild(element); + editorContainerDom.head.removeChild(element); }; // editorReady is here because TinyMCE starts by initializing a blank iframe, which needs to be // styled by us, otherwise users in dark mode get a bright white flash. During initialization @@ -593,7 +597,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { // // tl;dr: editorReady is used here because the css needs to be re-applied after TinyMCE init // eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied - }, [editorReady, props.themeId, lightTheme, props.whiteBackgroundNoteRendering, props.watchedNoteFiles]); + }, [editorReady, editorContainerDom, props.themeId, lightTheme, props.whiteBackgroundNoteRendering, props.watchedNoteFiles]); // ----------------------------------------------------------------------------------------- // Enable or disable the editor @@ -610,6 +614,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { useEffect(() => { if (!scriptLoaded) return; + if (!editorContainer) return; const loadEditor = async () => { const language = closestSupportedLocale(props.locale, true, supportedLocales); @@ -644,8 +649,9 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { ]; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - const editors = await (window as any).tinymce.init({ - selector: `#${rootIdRef.current}`, + const containerWindow = editorContainerDom.defaultView as any; + const editors = await containerWindow.tinymce.init({ + selector: `#${editorContainer.id}`, width: '100%', body_class: 'jop-tinymce', height: '100%', @@ -830,7 +836,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { void loadEditor(); // eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied - }, [scriptLoaded]); + }, [scriptLoaded, editorContainer]); // ----------------------------------------------------------------------------------------- // Set the initial content and load the plugin CSS and JS files @@ -1420,12 +1426,15 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { ); } + const containerId = useMemo(() => { + return `tinymce-container-${Math.ceil(Math.random() * 1000)}-${Date.now()}`; + }, []); return (
{renderDisabledOverlay()} {renderLeftExtraToolbarButtons()} {renderRightExtraToolbarButtons()} -
+
); }; diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.ts index 3ca36e681de..8ea5dd5cf9b 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.ts +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.ts @@ -2,13 +2,14 @@ import PluginService from '@joplin/lib/services/plugins/PluginService'; import { useEffect } from 'react'; import { Editor } from 'tinymce'; -const useWebViewApi = (editor: Editor) => { +const useWebViewApi = (editor: Editor, window: Window) => { useEffect(() => { if (!editor) return ()=>{}; + if (!window) return ()=>{}; - const scriptElement = document.createElement('script'); + const scriptElement = window.document.createElement('script'); const channelId = `plugin-post-message-${Math.random()}`; - scriptElement.appendChild(document.createTextNode(` + scriptElement.appendChild(window.document.createTextNode(` window.webviewApi = { postMessage: (contentScriptId, message) => { const channelId = ${JSON.stringify(channelId)}; @@ -66,7 +67,7 @@ const useWebViewApi = (editor: Editor) => { window.removeEventListener('message', onMessageHandler); scriptElement.remove(); }; - }, [editor]); + }, [editor, window]); }; export default useWebViewApi; diff --git a/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx b/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx new file mode 100644 index 00000000000..7785d2207b7 --- /dev/null +++ b/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx @@ -0,0 +1,87 @@ +import * as React from 'react'; +import { useState, useEffect } from 'react'; +import { createPortal } from 'react-dom'; +import NoteEditor from './NoteEditor'; +import StyleSheetContainer from '../StyleSheets/StyleSheetContainer'; +import { connect } from 'react-redux'; +import { AppState } from '../../app.reducer'; + +interface Props { + themeId: number; + bodyEditor: string; + newWindow: boolean; +} + +const NoteEditorWrapper: React.FC = props => { + const [iframeRef, setIframeRef] = useState(null); + const [loaded, setLoaded] = useState(false); + const [doc, setDoc] = useState(null); + + useEffect(() => { + let openedWindow: Window|null = null; + if (iframeRef) { + setDoc(iframeRef?.contentWindow?.document); + } else if (props.newWindow) { + openedWindow = window.open('about:blank'); + setDoc(openedWindow.document); + } + + return () => { + // Delay: Closing immediately causes Electron to crash + setTimeout(() => openedWindow?.close(), 200); + }; + }, [iframeRef, props.newWindow]); + + useEffect(() => { + if (!doc) return; + + doc.open(); + doc.write(''); + doc.close(); + + const cssUrls = [ + 'style.min.css', + 'style/icons/style.css', + 'vendor/lib/@fortawesome/fontawesome-free/css/all.min.css', + 'vendor/lib/react-datetime/css/react-datetime.css', + 'vendor/lib/smalltalk/css/smalltalk.css', + 'vendor/lib/roboto-fontface/css/roboto/roboto-fontface.css', + 'vendor/lib/codemirror/lib/codemirror.css', + ]; + + for (const url of cssUrls) { + const style = doc.createElement('link'); + style.rel = 'stylesheet'; + style.href = url; + doc.head.appendChild(style); + } + + doc.body.style.height = '100vh'; + + setLoaded(true); + }, [doc]); + const parentNode = loaded ? doc?.body : null; + + const content = <> + + + ; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Needed to allow adding the portal to the DOM + const contentPortal = parentNode && createPortal(content, parentNode) as any; + if (props.newWindow) { + return
{contentPortal}
; + } else { + return ; + } +}; + +export default connect((state: AppState) => { + return { + themeId: state.settings.theme, + }; +})(NoteEditorWrapper); diff --git a/packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.tsx b/packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.tsx index 91b537bd9db..a863efc30a0 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.tsx @@ -5,30 +5,6 @@ import { ChangeEvent, useCallback, useRef } from 'react'; import NoteToolbar from '../../NoteToolbar/NoteToolbar'; import { buildStyle } from '@joplin/lib/theme'; import time from '@joplin/lib/time'; -import styled from 'styled-components'; - -const StyledRoot = styled.div` - display: flex; - flex-direction: row; - align-items: center; - padding-left: ${props => props.theme.editorPaddingLeft}px; - - @media (max-width: 800px) { - flex-direction: column; - align-items: flex-start; - } -`; - -const InfoGroup = styled.div` - display: flex; - flex-direction: row; - align-items: center; - - @media (max-width: 800px) { - border-top: 1px solid ${props => props.theme.dividerColor}; - width: 100%; - } -`; interface Props { themeId: number; @@ -130,7 +106,7 @@ export default function NoteTitleBar(props: Props) { } return ( - +
- +
{renderTitleBarDate()} {renderNoteToolbar()} - - +
+
); } diff --git a/packages/app-desktop/gui/NoteEditor/style.scss b/packages/app-desktop/gui/NoteEditor/style.scss index 6901cdbc67f..6fe97444de1 100644 --- a/packages/app-desktop/gui/NoteEditor/style.scss +++ b/packages/app-desktop/gui/NoteEditor/style.scss @@ -1,3 +1,5 @@ @use "./styles/warning-banner.scss"; @use "./styles/warning-banner-link.scss"; +@use "./styles/note-title-info-group.scss"; +@use "./styles/note-title-wrapper.scss"; diff --git a/packages/app-desktop/gui/NoteEditor/styles/note-title-info-group.scss b/packages/app-desktop/gui/NoteEditor/styles/note-title-info-group.scss new file mode 100644 index 00000000000..70886d143e1 --- /dev/null +++ b/packages/app-desktop/gui/NoteEditor/styles/note-title-info-group.scss @@ -0,0 +1,11 @@ + +.note-title-info-group { + display: flex; + flex-direction: row; + align-items: center; + + @media (max-width: 800px) { + border-top: 1px solid var(--joplin-divider-color); + width: 100%; + } +} \ No newline at end of file diff --git a/packages/app-desktop/gui/NoteEditor/styles/note-title-wrapper.scss b/packages/app-desktop/gui/NoteEditor/styles/note-title-wrapper.scss new file mode 100644 index 00000000000..4119c51f539 --- /dev/null +++ b/packages/app-desktop/gui/NoteEditor/styles/note-title-wrapper.scss @@ -0,0 +1,12 @@ + +.note-title-wrapper { + display: flex; + flex-direction: row; + align-items: center; + padding-left: var(--joplin-editor-padding-left); + + @media (max-width: 800px) { + flex-direction: column; + align-items: flex-start; + } +} \ No newline at end of file diff --git a/packages/app-desktop/gui/NoteTextViewer.tsx b/packages/app-desktop/gui/NoteTextViewer.tsx index debb4451eb5..4a20efc7162 100644 --- a/packages/app-desktop/gui/NoteTextViewer.tsx +++ b/packages/app-desktop/gui/NoteTextViewer.tsx @@ -90,6 +90,12 @@ export default class NoteTextViewerComponent extends React.Component return this.domReady_; } + private getContainerWindow() { + // Handles the case where this.webviewRef_ doesn't point to an element in the global Document. + const containerDoc = this.webviewRef_.current.getRootNode() as Document; + return containerDoc.defaultView; + } + public initWebview() { const wv = this.webviewRef_.current; @@ -107,7 +113,7 @@ export default class NoteTextViewerComponent extends React.Component wv.addEventListener(n, fn); } - window.addEventListener('message', this.webview_message); + this.getContainerWindow().addEventListener('message', this.webview_message); } public destroyWebview() { @@ -120,7 +126,7 @@ export default class NoteTextViewerComponent extends React.Component wv.removeEventListener(n, fn); } - window.removeEventListener('message', this.webview_message); + this.getContainerWindow().removeEventListener('message', this.webview_message); this.initialized_ = false; this.domReady_ = false; diff --git a/packages/app-desktop/gui/StyleSheets/StyleSheetContainer.tsx b/packages/app-desktop/gui/StyleSheets/StyleSheetContainer.tsx index d4d941ac0ef..b647cdfdca8 100644 --- a/packages/app-desktop/gui/StyleSheets/StyleSheetContainer.tsx +++ b/packages/app-desktop/gui/StyleSheets/StyleSheetContainer.tsx @@ -12,6 +12,7 @@ import { useEffect, useState } from 'react'; import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect'; import themeToCss from '@joplin/lib/services/style/themeToCss'; import { themeStyle } from '@joplin/lib/theme'; +import useDom from '../hooks/useDom'; interface Props { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied @@ -21,6 +22,8 @@ interface Props { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied export default function(props: Props): any { const [styleSheetContent, setStyleSheetContent] = useState(''); + const [elementRef, setElementRef] = useState(null); + const doc = useDom(elementRef); useAsyncEffect(async (event: AsyncEffectEvent) => { const theme = themeStyle(props.themeId); @@ -30,14 +33,16 @@ export default function(props: Props): any { }, [props.themeId]); useEffect(() => { - const element = document.createElement('style'); + if (!doc) return () => {}; + + const element = doc.createElement('style'); element.setAttribute('id', 'main-theme-stylesheet-container'); - document.head.appendChild(element); + doc.head.appendChild(element); element.appendChild(document.createTextNode(styleSheetContent)); return () => { - document.head.removeChild(element); + doc.head.removeChild(element); }; - }, [styleSheetContent]); + }, [styleSheetContent, doc]); - return
; + return
; } diff --git a/packages/app-desktop/gui/ToolbarBase.tsx b/packages/app-desktop/gui/ToolbarBase.tsx index 0ccb4e8ac0f..f55451a7ae0 100644 --- a/packages/app-desktop/gui/ToolbarBase.tsx +++ b/packages/app-desktop/gui/ToolbarBase.tsx @@ -7,6 +7,7 @@ import { AppState } from '../app.reducer'; import { connect } from 'react-redux'; import { useCallback, useMemo, useRef, useState } from 'react'; import { focus } from '@joplin/lib/utils/focusHandler'; +import useDom from './hooks/useDom'; interface ToolbarItemInfo extends ToolbarButtonInfo { type?: string; @@ -105,8 +106,11 @@ const ToolbarBaseComponent: React.FC = props => { const focusableItems = useMemo(() => { return allItems.filter(isFocusable); }, [allItems]); - const containerRef = useRef(null); - const containerHasFocus = !!containerRef.current?.contains(document.activeElement); + const [containerNode, setContainerNode] = useState(null); + const containerRef = useRef(containerNode); + containerRef.current = containerNode; + const doc = useDom(containerNode); + const containerHasFocus = !!containerRef.current?.contains(doc.activeElement); let keyCounter = 0; const renderItem = (o: ToolbarItemInfo, indexInFocusable: number) => { @@ -180,7 +184,7 @@ const ToolbarBaseComponent: React.FC = props => { return (
{ + return useMemo(() => { + return (elementRef ?? null)?.getRootNode() as Document|null; + }, [elementRef]); +}; + +export default useDom; diff --git a/packages/app-desktop/gui/utils/loadScript.ts b/packages/app-desktop/gui/utils/loadScript.ts index 6527c653b36..15ef16e53c7 100644 --- a/packages/app-desktop/gui/utils/loadScript.ts +++ b/packages/app-desktop/gui/utils/loadScript.ts @@ -9,7 +9,7 @@ export interface Script { attrs?: Record; } -export const loadScript = async (script: Script) => { +export const loadScript = async (script: Script, document: Document) => { return new Promise((resolve) => { // eslint-disable-next-line no-console console.info('Loading script:', script); diff --git a/packages/app-desktop/index.html b/packages/app-desktop/index.html index e1b54ed2c94..538c922cdcb 100644 --- a/packages/app-desktop/index.html +++ b/packages/app-desktop/index.html @@ -20,41 +20,10 @@ - -
- - diff --git a/packages/app-desktop/main.scss b/packages/app-desktop/main.scss index 081330db61a..c25a1f10791 100644 --- a/packages/app-desktop/main.scss +++ b/packages/app-desktop/main.scss @@ -4,6 +4,10 @@ body, textarea { border: none; } +:root { + background-color: var(--joplin-background-color); +} + #react-root { height: 100%; overflow: hidden; @@ -308,3 +312,31 @@ Component-specific classes color: var(--joplin-color-error); margin-left: 5px; } + + +// Disable dragging of links (which are often buttons) +a:not([draggable=true]), img:not([draggable=true]) { + -webkit-user-drag: none; +} + +.smalltalk { + background-color: rgba(0,0,0,.5); +} +.smalltalk input { + margin-top: 1em; +} +.smalltalk .page { + max-width: 30em; +} +mark { + background: #CF3F00; + color: white; +} + +/* + Adds support for RTL text in the note body. It automatically detects the direction using the content. + Issue: https://github.com/laurent22/joplin/issues/3991 +*/ +.CodeMirror-line { + unicode-bidi: plaintext; +} \ No newline at end of file From 63de97d3bcbb2181a78d2db7b78d19c8cb29a314 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Sat, 5 Oct 2024 09:49:10 -0700 Subject: [PATCH 03/97] Allow opening notes in new windows --- .eslintignore | 1 + .gitignore | 1 + packages/app-desktop/app.reducer.ts | 12 +++++ packages/app-desktop/commands/index.ts | 2 + .../commands/openNoteInNewWindow.ts | 19 ++++++++ .../app-desktop/gui/MainScreen/MainScreen.tsx | 16 +++---- .../app-desktop/gui/NoteEditor/NoteEditor.tsx | 3 -- .../gui/NoteEditor/NoteEditorWrapper.tsx | 47 +++++++++++++++++-- packages/app-desktop/gui/NoteTextViewer.tsx | 2 +- 9 files changed, 88 insertions(+), 15 deletions(-) create mode 100644 packages/app-desktop/commands/openNoteInNewWindow.ts diff --git a/.eslintignore b/.eslintignore index 42526afaba2..60564a485df 100644 --- a/.eslintignore +++ b/.eslintignore @@ -154,6 +154,7 @@ packages/app-desktop/commands/exportFolders.js packages/app-desktop/commands/exportNotes.js packages/app-desktop/commands/focusElement.js packages/app-desktop/commands/index.js +packages/app-desktop/commands/openNoteInNewWindow.js packages/app-desktop/commands/openProfileDirectory.js packages/app-desktop/commands/replaceMisspelling.js packages/app-desktop/commands/restoreNoteRevision.js diff --git a/.gitignore b/.gitignore index 3526f5b33c5..4685ef157e1 100644 --- a/.gitignore +++ b/.gitignore @@ -131,6 +131,7 @@ packages/app-desktop/commands/exportFolders.js packages/app-desktop/commands/exportNotes.js packages/app-desktop/commands/focusElement.js packages/app-desktop/commands/index.js +packages/app-desktop/commands/openNoteInNewWindow.js packages/app-desktop/commands/openProfileDirectory.js packages/app-desktop/commands/replaceMisspelling.js packages/app-desktop/commands/restoreNoteRevision.js diff --git a/packages/app-desktop/app.reducer.ts b/packages/app-desktop/app.reducer.ts index 35711b19b2e..77d3877f22e 100644 --- a/packages/app-desktop/app.reducer.ts +++ b/packages/app-desktop/app.reducer.ts @@ -38,6 +38,7 @@ export interface AppState extends State { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied windowContentSize: any; watchedNoteFiles: string[]; + newWindowNoteIds: string[]; lastEditorScrollPercents: EditorScrollPercents; devToolsVisible: boolean; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied @@ -67,6 +68,7 @@ export function createAppDefaultState(windowContentSize: any, resourceEditWatche noteVisiblePanes: ['editor', 'viewer'], windowContentSize, // bridge().windowContentSize(), watchedNoteFiles: [], + newWindowNoteIds: [], lastEditorScrollPercents: {}, devToolsVisible: false, visibleDialogs: {}, // empty object if no dialog is visible. Otherwise contains the list of visible dialogs. @@ -248,6 +250,16 @@ export default function(state: AppState, action: any) { } break; + case 'NOTE_WINDOW_OPEN': + if (!state.newWindowNoteIds.includes(action.noteId)) { + newState = { ...state, newWindowNoteIds: [...state.newWindowNoteIds, action.noteId] }; + } + break; + + case 'NOTE_WINDOW_CLOSE': + newState = { ...state, newWindowNoteIds: state.newWindowNoteIds.filter(id => id !== action.noteId) }; + break; + case 'EDITOR_SCROLL_PERCENT_SET': { diff --git a/packages/app-desktop/commands/index.ts b/packages/app-desktop/commands/index.ts index 449a3ddbe70..fb34cb3c986 100644 --- a/packages/app-desktop/commands/index.ts +++ b/packages/app-desktop/commands/index.ts @@ -5,6 +5,7 @@ import * as emptyTrash from './emptyTrash'; import * as exportFolders from './exportFolders'; import * as exportNotes from './exportNotes'; import * as focusElement from './focusElement'; +import * as openNoteInNewWindow from './openNoteInNewWindow'; import * as openProfileDirectory from './openProfileDirectory'; import * as replaceMisspelling from './replaceMisspelling'; import * as restoreNoteRevision from './restoreNoteRevision'; @@ -24,6 +25,7 @@ const index: any[] = [ exportFolders, exportNotes, focusElement, + openNoteInNewWindow, openProfileDirectory, replaceMisspelling, restoreNoteRevision, diff --git a/packages/app-desktop/commands/openNoteInNewWindow.ts b/packages/app-desktop/commands/openNoteInNewWindow.ts new file mode 100644 index 00000000000..dc6c8e2b9e2 --- /dev/null +++ b/packages/app-desktop/commands/openNoteInNewWindow.ts @@ -0,0 +1,19 @@ +import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService'; +import { _ } from '@joplin/lib/locale'; +import { stateUtils } from '@joplin/lib/reducer'; + +export const declaration: CommandDeclaration = { + name: 'openNoteInNewWindow', + label: () => _('Edit in new window'), + iconName: 'icon-share', +}; + +export const runtime = (): CommandRuntime => { + return { + execute: async (context: CommandContext, noteId: string = null) => { + noteId = noteId || stateUtils.selectedNoteId(context.state); + context.dispatch({ type: 'NOTE_WINDOW_OPEN', noteId }); + }, + enabledCondition: 'oneNoteSelected', + }; +}; diff --git a/packages/app-desktop/gui/MainScreen/MainScreen.tsx b/packages/app-desktop/gui/MainScreen/MainScreen.tsx index bb509716056..614e2610852 100644 --- a/packages/app-desktop/gui/MainScreen/MainScreen.tsx +++ b/packages/app-desktop/gui/MainScreen/MainScreen.tsx @@ -66,6 +66,7 @@ interface Props { // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied dispatch: Function; mainLayout: LayoutItem; + newWindowNoteIds: string[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied style: any; layoutMoveMode: boolean; @@ -784,14 +785,7 @@ class MainScreenComponent extends React.Component { }, editor: () => { - let bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror6' : 'TinyMCE'; - - if (this.props.isSafeMode) { - bodyEditor = 'PlainText'; - } else if (this.props.settingEditorCodeView && this.props.enableLegacyMarkdownEditor) { - bodyEditor = 'CodeMirror5'; - } - return ; + return ; }, }; @@ -918,6 +912,10 @@ class MainScreenComponent extends React.Component { /> ) : null; + const newWindowNotes = this.props.newWindowNoteIds.map(noteId => { + return ; + }); + return (
{this.state.modalLayer.message}
@@ -940,6 +938,7 @@ class MainScreenComponent extends React.Component { {messageComp} {layoutComp} {pluginDialog} + {newWindowNotes}
); } @@ -983,6 +982,7 @@ const mapStateToProps = (state: AppState) => { notesSortOrderReverse: state.settings['notes.sortOrder.reverse'], notesColumns: validateColumns(state.settings['notes.columns']), showInvalidJoplinCloudCredential: state.settings['sync.target'] === 10 && state.mustAuthenticate, + newWindowNoteIds: state.newWindowNoteIds, }; }; diff --git a/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx b/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx index d832166bab4..74cce65a3c8 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx @@ -611,15 +611,12 @@ export { }; const mapStateToProps = (state: AppState) => { - const noteId = state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null; const whenClauseContext = stateToWhenClauseContext(state); return { - noteId: noteId, notes: state.notes, selectedNoteIds: state.selectedNoteIds, selectedFolderId: state.selectedFolderId, - isProvisional: state.provisionalNoteIds.includes(noteId), editorNoteStatuses: state.editorNoteStatuses, syncStarted: state.syncStarted, decryptionStarted: state.decryptionWorker?.state !== 'idle', diff --git a/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx b/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx index 7785d2207b7..043d34c921a 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx @@ -5,11 +5,17 @@ import NoteEditor from './NoteEditor'; import StyleSheetContainer from '../StyleSheets/StyleSheetContainer'; import { connect } from 'react-redux'; import { AppState } from '../../app.reducer'; +import { Dispatch } from 'redux'; +import shim from '@joplin/lib/shim'; interface Props { + dispatch: Dispatch; themeId: number; bodyEditor: string; newWindow: boolean; + noteId?: string; + defaultNoteId: string; + provisionalNoteIds: string[]; } const NoteEditorWrapper: React.FC = props => { @@ -19,18 +25,39 @@ const NoteEditorWrapper: React.FC = props => { useEffect(() => { let openedWindow: Window|null = null; + const unmounted = false; if (iframeRef) { setDoc(iframeRef?.contentWindow?.document); } else if (props.newWindow) { openedWindow = window.open('about:blank'); setDoc(openedWindow.document); + + void (async () => { + while (!unmounted) { + await new Promise(resolve => { + shim.setTimeout(() => resolve(), 2000); + }); + + // .onbeforeunload and .onclose events don't seem to fire when closed by a user -- rely on polling + // instead: + if (openedWindow?.closed) { + props.dispatch({ type: 'NOTE_WINDOW_CLOSE', noteId: props.noteId }); + openedWindow = null; + break; + } + } + })(); } return () => { // Delay: Closing immediately causes Electron to crash - setTimeout(() => openedWindow?.close(), 200); + setTimeout(() => { + if (!openedWindow?.closed) { + openedWindow?.close(); + } + }, 200); }; - }, [iframeRef, props.newWindow]); + }, [iframeRef, props.newWindow, props.noteId, props.dispatch]); useEffect(() => { if (!doc) return; @@ -62,8 +89,9 @@ const NoteEditorWrapper: React.FC = props => { }, [doc]); const parentNode = loaded ? doc?.body : null; + const noteId = props.noteId ?? props.defaultNoteId; const content = <> - + ; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Needed to allow adding the portal to the DOM @@ -81,7 +109,20 @@ const NoteEditorWrapper: React.FC = props => { }; export default connect((state: AppState) => { + let bodyEditor = state.settings['editor.codeView'] ? 'CodeMirror6' : 'TinyMCE'; + + if (state.settings.isSafeMode) { + bodyEditor = 'PlainText'; + } else if (state.settings['editor.codeView'] && state.settings['editor.legacyMarkdown']) { + bodyEditor = 'CodeMirror5'; + } + + const defaultNoteId = state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null; + return { themeId: state.settings.theme, + provisionalNoteIds: state.provisionalNoteIds, + defaultNoteId: defaultNoteId, + bodyEditor, }; })(NoteEditorWrapper); diff --git a/packages/app-desktop/gui/NoteTextViewer.tsx b/packages/app-desktop/gui/NoteTextViewer.tsx index 4a20efc7162..b212274334c 100644 --- a/packages/app-desktop/gui/NoteTextViewer.tsx +++ b/packages/app-desktop/gui/NoteTextViewer.tsx @@ -126,7 +126,7 @@ export default class NoteTextViewerComponent extends React.Component wv.removeEventListener(n, fn); } - this.getContainerWindow().removeEventListener('message', this.webview_message); + this.getContainerWindow()?.removeEventListener('message', this.webview_message); this.initialized_ = false; this.domReady_ = false; From 954b3821bc3ae98a53376a88bbf3225ea0105fc2 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Sat, 5 Oct 2024 10:35:27 -0700 Subject: [PATCH 04/97] Refactoring & style fixes --- .eslintignore | 2 +- .gitignore | 2 +- packages/app-desktop/app.ts | 21 ---- .../app-desktop/gui/NewWindowOrIFrame.tsx | 108 ++++++++++++++++++ .../gui/NoteEditor/NoteEditorWrapper.tsx | 101 +++------------- packages/app-desktop/gui/Root.tsx | 2 +- .../gui/StyleSheets/StyleSheetContainer.tsx | 55 +++++++-- .../gui/ToolbarButton/ToolbarButton.tsx | 9 +- .../gui/ToolbarButton/styles/index.ts | 19 --- packages/app-desktop/gui/styles/index.scss | 1 + .../app-desktop/gui/styles/toolbar-icon.scss | 11 ++ .../app-desktop/gui/utils/NoteListUtils.ts | 10 +- 12 files changed, 197 insertions(+), 144 deletions(-) create mode 100644 packages/app-desktop/gui/NewWindowOrIFrame.tsx delete mode 100644 packages/app-desktop/gui/ToolbarButton/styles/index.ts create mode 100644 packages/app-desktop/gui/styles/toolbar-icon.scss diff --git a/.eslintignore b/.eslintignore index 60564a485df..dbe7fa49184 100644 --- a/.eslintignore +++ b/.eslintignore @@ -259,6 +259,7 @@ packages/app-desktop/gui/MasterPasswordDialog/Dialog.js packages/app-desktop/gui/MenuBar.js packages/app-desktop/gui/MultiNoteActions.js packages/app-desktop/gui/Navigator.js +packages/app-desktop/gui/NewWindowOrIFrame.js packages/app-desktop/gui/NoteContentPropertiesDialog.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js @@ -443,7 +444,6 @@ packages/app-desktop/gui/ToggleEditorsButton/ToggleEditorsButton.js packages/app-desktop/gui/ToggleEditorsButton/styles/index.js packages/app-desktop/gui/ToolbarBase.js packages/app-desktop/gui/ToolbarButton/ToolbarButton.js -packages/app-desktop/gui/ToolbarButton/styles/index.js packages/app-desktop/gui/ToolbarSpace.js packages/app-desktop/gui/TrashNotification/TrashNotification.js packages/app-desktop/gui/UpdateNotification/UpdateNotification.js diff --git a/.gitignore b/.gitignore index 4685ef157e1..f06a7fc62a0 100644 --- a/.gitignore +++ b/.gitignore @@ -236,6 +236,7 @@ packages/app-desktop/gui/MasterPasswordDialog/Dialog.js packages/app-desktop/gui/MenuBar.js packages/app-desktop/gui/MultiNoteActions.js packages/app-desktop/gui/Navigator.js +packages/app-desktop/gui/NewWindowOrIFrame.js packages/app-desktop/gui/NoteContentPropertiesDialog.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/Toolbar.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js @@ -420,7 +421,6 @@ packages/app-desktop/gui/ToggleEditorsButton/ToggleEditorsButton.js packages/app-desktop/gui/ToggleEditorsButton/styles/index.js packages/app-desktop/gui/ToolbarBase.js packages/app-desktop/gui/ToolbarButton/ToolbarButton.js -packages/app-desktop/gui/ToolbarButton/styles/index.js packages/app-desktop/gui/ToolbarSpace.js packages/app-desktop/gui/TrashNotification/TrashNotification.js packages/app-desktop/gui/UpdateNotification/UpdateNotification.js diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index 31109526969..710c6892f1b 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -135,10 +135,6 @@ class Application extends BaseApplication { void this.setupOcrService(); } - if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'style.editor.fontFamily' || action.type === 'SETTING_UPDATE_ALL') { - this.updateEditorFont(); - } - if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'windowContentZoomFactor' || action.type === 'SETTING_UPDATE_ALL') { webFrame.setZoomFactor(Setting.value('windowContentZoomFactor') / 100); } @@ -210,23 +206,6 @@ class Application extends BaseApplication { } } - public updateEditorFont() { - const fontFamilies = []; - if (Setting.value('style.editor.fontFamily')) fontFamilies.push(`"${Setting.value('style.editor.fontFamily')}"`); - fontFamilies.push('\'Avenir Next\', Avenir, Arial, sans-serif'); - - // The '*' and '!important' parts are necessary to make sure Russian text is displayed properly - // https://github.com/laurent22/joplin/issues/155 - // - // Note: Be careful about the specificity here. Incorrect specificity can break monospaced fonts in tables. - - const css = `.CodeMirror5 *, .cm-editor .cm-content { font-family: ${fontFamilies.join(', ')} !important; }`; - const styleTag = document.createElement('style'); - styleTag.type = 'text/css'; - styleTag.appendChild(document.createTextNode(css)); - document.head.appendChild(styleTag); - } - public setupContextMenu() { // bridge().setupContextMenu((misspelledWord: string, dictionarySuggestions: string[]) => { // let output = SpellCheckerService.instance().contextMenuItems(misspelledWord, dictionarySuggestions); diff --git a/packages/app-desktop/gui/NewWindowOrIFrame.tsx b/packages/app-desktop/gui/NewWindowOrIFrame.tsx new file mode 100644 index 00000000000..4e8daea67aa --- /dev/null +++ b/packages/app-desktop/gui/NewWindowOrIFrame.tsx @@ -0,0 +1,108 @@ +import shim from '@joplin/lib/shim'; +import * as React from 'react'; +import { useState, useEffect, useRef } from 'react'; +import { createPortal } from 'react-dom'; + +interface Props { + // Note: children will be rendered in a different DOM from this node. Avoid using document.* methods + // in child components. + children: React.ReactNode[]|React.ReactNode; + newWindow: boolean; + onClose: ()=> void; +} + +const NewWindowOrIFrame: React.FC = props => { + const [iframeRef, setIframeRef] = useState(null); + const [loaded, setLoaded] = useState(false); + const [doc, setDoc] = useState(null); + + const onCloseRef = useRef(props.onClose); + onCloseRef.current = props.onClose; + + useEffect(() => { + let openedWindow: Window|null = null; + const unmounted = false; + if (iframeRef) { + setDoc(iframeRef?.contentWindow?.document); + } else if (props.newWindow) { + openedWindow = window.open('about:blank'); + setDoc(openedWindow.document); + + // .onbeforeunload and .onclose events don't seem to fire when closed by a user -- rely on polling + // instead: + void (async () => { + while (!unmounted) { + await new Promise(resolve => { + shim.setTimeout(() => resolve(), 2000); + }); + + if (openedWindow?.closed) { + onCloseRef.current?.(); + openedWindow = null; + break; + } + } + })(); + } + + return () => { + // Delay: Closing immediately causes Electron to crash + setTimeout(() => { + if (!openedWindow?.closed) { + openedWindow?.close(); + onCloseRef.current?.(); + openedWindow = null; + } + }, 200); + + if (iframeRef && !openedWindow) { + onCloseRef.current?.(); + } + }; + }, [iframeRef, props.newWindow]); + + useEffect(() => { + if (!doc) return; + + doc.open(); + doc.write(''); + doc.close(); + + const cssUrls = [ + 'style.min.css', + 'style/icons/style.css', + 'vendor/lib/@fortawesome/fontawesome-free/css/all.min.css', + 'vendor/lib/react-datetime/css/react-datetime.css', + 'vendor/lib/smalltalk/css/smalltalk.css', + 'vendor/lib/roboto-fontface/css/roboto/roboto-fontface.css', + 'vendor/lib/codemirror/lib/codemirror.css', + ]; + + for (const url of cssUrls) { + const style = doc.createElement('link'); + style.rel = 'stylesheet'; + style.href = url; + doc.head.appendChild(style); + } + + doc.body.style.height = '100vh'; + + setLoaded(true); + }, [doc]); + const parentNode = loaded ? doc?.body : null; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Needed to allow adding the portal to the DOM + const contentPortal = parentNode && createPortal(props.children, parentNode) as any; + if (props.newWindow) { + return
{contentPortal}
; + } else { + return ; + } +}; + +export default NewWindowOrIFrame; diff --git a/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx b/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx index 043d34c921a..741ced4da55 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx @@ -1,12 +1,11 @@ import * as React from 'react'; -import { useState, useEffect } from 'react'; -import { createPortal } from 'react-dom'; +import { useCallback } from 'react'; import NoteEditor from './NoteEditor'; import StyleSheetContainer from '../StyleSheets/StyleSheetContainer'; import { connect } from 'react-redux'; import { AppState } from '../../app.reducer'; import { Dispatch } from 'redux'; -import shim from '@joplin/lib/shim'; +import NewWindowOrIFrame from '../NewWindowOrIFrame'; interface Props { dispatch: Dispatch; @@ -19,93 +18,19 @@ interface Props { } const NoteEditorWrapper: React.FC = props => { - const [iframeRef, setIframeRef] = useState(null); - const [loaded, setLoaded] = useState(false); - const [doc, setDoc] = useState(null); - - useEffect(() => { - let openedWindow: Window|null = null; - const unmounted = false; - if (iframeRef) { - setDoc(iframeRef?.contentWindow?.document); - } else if (props.newWindow) { - openedWindow = window.open('about:blank'); - setDoc(openedWindow.document); - - void (async () => { - while (!unmounted) { - await new Promise(resolve => { - shim.setTimeout(() => resolve(), 2000); - }); - - // .onbeforeunload and .onclose events don't seem to fire when closed by a user -- rely on polling - // instead: - if (openedWindow?.closed) { - props.dispatch({ type: 'NOTE_WINDOW_CLOSE', noteId: props.noteId }); - openedWindow = null; - break; - } - } - })(); - } - - return () => { - // Delay: Closing immediately causes Electron to crash - setTimeout(() => { - if (!openedWindow?.closed) { - openedWindow?.close(); - } - }, 200); - }; - }, [iframeRef, props.newWindow, props.noteId, props.dispatch]); - - useEffect(() => { - if (!doc) return; - - doc.open(); - doc.write(''); - doc.close(); - - const cssUrls = [ - 'style.min.css', - 'style/icons/style.css', - 'vendor/lib/@fortawesome/fontawesome-free/css/all.min.css', - 'vendor/lib/react-datetime/css/react-datetime.css', - 'vendor/lib/smalltalk/css/smalltalk.css', - 'vendor/lib/roboto-fontface/css/roboto/roboto-fontface.css', - 'vendor/lib/codemirror/lib/codemirror.css', - ]; - - for (const url of cssUrls) { - const style = doc.createElement('link'); - style.rel = 'stylesheet'; - style.href = url; - doc.head.appendChild(style); - } - - doc.body.style.height = '100vh'; - - setLoaded(true); - }, [doc]); - const parentNode = loaded ? doc?.body : null; + const onClose = useCallback(() => { + props.dispatch({ type: 'NOTE_WINDOW_CLOSE', noteId: props.noteId }); + }, [props.dispatch, props.noteId]); const noteId = props.noteId ?? props.defaultNoteId; - const content = <> - - - ; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Needed to allow adding the portal to the DOM - const contentPortal = parentNode && createPortal(content, parentNode) as any; - if (props.newWindow) { - return
{contentPortal}
; - } else { - return ; - } + const editor = ; + + // TODO: Always render the editor in an ; } export default forwardRef(UserWebview); diff --git a/packages/app-desktop/services/plugins/UserWebviewDialog.tsx b/packages/app-desktop/services/plugins/UserWebviewDialog.tsx index 942d63da456..6c74eb419d5 100644 --- a/packages/app-desktop/services/plugins/UserWebviewDialog.tsx +++ b/packages/app-desktop/services/plugins/UserWebviewDialog.tsx @@ -7,18 +7,12 @@ import UserWebview, { Props as UserWebviewProps } from './UserWebview'; import UserWebviewDialogButtonBar from './UserWebviewDialogButtonBar'; import { focus } from '@joplin/lib/utils/focusHandler'; import Dialog from '../../gui/Dialog'; -const styled = require('styled-components').default; interface Props extends UserWebviewProps { buttons: ButtonSpec[]; fitToContent: boolean; } -const UserWebViewWrapper = styled.div` - display: flex; - flex: 1; -`; - function defaultButtons(): ButtonSpec[] { return [ { @@ -84,7 +78,7 @@ export default function UserWebviewDialog(props: Props) { return ( - +
- +
); diff --git a/packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.tsx b/packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.tsx index 0dc646e1e5a..6bd32190359 100644 --- a/packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.tsx +++ b/packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.tsx @@ -5,20 +5,10 @@ import { ButtonSpec } from '@joplin/lib/services/plugins/api/types'; const styled = require('styled-components').default; const { space } = require('styled-system'); -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied -type StyleProps = any; - interface Props { buttons: ButtonSpec[]; } -const StyledRoot = styled.div` - display: flex; - width: 100%; - box-sizing: border-box; - justify-content: flex-end; - padding-top: ${(props: StyleProps) => props.theme.mainPadding}px; -`; const StyledButton = styled(Button)`${space}`; @@ -48,8 +38,8 @@ export default function UserWebviewDialogButtonBar(props: Props) { } return ( - +
{renderButtons()} - +
); } diff --git a/packages/app-desktop/services/plugins/styles/index.scss b/packages/app-desktop/services/plugins/styles/index.scss new file mode 100644 index 00000000000..fc2b1227093 --- /dev/null +++ b/packages/app-desktop/services/plugins/styles/index.scss @@ -0,0 +1,3 @@ +@use './plugin-user-webview.scss'; +@use './user-dialog-wrapper.scss'; +@use './user-dialog-button-bar.scss'; diff --git a/packages/app-desktop/services/plugins/styles/plugin-user-webview.scss b/packages/app-desktop/services/plugins/styles/plugin-user-webview.scss new file mode 100644 index 00000000000..43a8e732cbb --- /dev/null +++ b/packages/app-desktop/services/plugins/styles/plugin-user-webview.scss @@ -0,0 +1,17 @@ + +.plugin-user-webview { + padding: 0; + margin: 0; + border: none; + width: 100%; + height: 100%; + + &.-border-bottom { + border-bottom: 1px solid var(--joplin-divider-color); + } + + &.-fit-to-content { + width: var(--content-width); + height: var(--content-height); + } +} diff --git a/packages/app-desktop/services/plugins/styles/user-dialog-button-bar.scss b/packages/app-desktop/services/plugins/styles/user-dialog-button-bar.scss new file mode 100644 index 00000000000..5e2dcfe44ed --- /dev/null +++ b/packages/app-desktop/services/plugins/styles/user-dialog-button-bar.scss @@ -0,0 +1,8 @@ + +.user-dialog-button-bar { + display: flex; + width: 100%; + box-sizing: border-box; + justify-content: flex-end; + padding-top: var(--joplin-main-padding); +} \ No newline at end of file diff --git a/packages/app-desktop/services/plugins/styles/user-dialog-wrapper.scss b/packages/app-desktop/services/plugins/styles/user-dialog-wrapper.scss new file mode 100644 index 00000000000..d001e28d746 --- /dev/null +++ b/packages/app-desktop/services/plugins/styles/user-dialog-wrapper.scss @@ -0,0 +1,5 @@ + +.user-dialog-wrapper { + display: flex; + flex: 1; +} diff --git a/packages/app-desktop/style.scss b/packages/app-desktop/style.scss index c42a3550367..3765576fde2 100644 --- a/packages/app-desktop/style.scss +++ b/packages/app-desktop/style.scss @@ -11,6 +11,7 @@ @use 'gui/UpdateNotification/style.scss' as update-notification; @use 'gui/TrashNotification/style.scss' as trash-notification; @use 'gui/Sidebar/style.scss' as sidebar-styles; -@use 'gui/styles/index.scss'; -@use 'gui/NoteEditor/style.scss'; +@use 'gui/NoteEditor/style.scss' as note-editor-styles; +@use 'services/plugins/styles/index.scss' as plugins-styles; +@use 'gui/styles/index.scss' as gui-styles; @use 'main.scss' as main; From fad84d47c7f8a6499778a48d1742643630bbe1fb Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 10 Oct 2024 09:32:02 -0700 Subject: [PATCH 11/97] Fix useFormNote now refreshes too often --- .../gui/NoteEditor/utils/useFormNote.ts | 167 +++++++++++------- 1 file changed, 99 insertions(+), 68 deletions(-) diff --git a/packages/app-desktop/gui/NoteEditor/utils/useFormNote.ts b/packages/app-desktop/gui/NoteEditor/utils/useFormNote.ts index 3f4e8a61d4d..c5319da8014 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/useFormNote.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/useFormNote.ts @@ -15,6 +15,7 @@ import { focus } from '@joplin/lib/utils/focusHandler'; import Logger from '@joplin/utils/Logger'; import eventManager, { EventName } from '@joplin/lib/eventManager'; import DecryptionWorker from '@joplin/lib/services/DecryptionWorker'; +import useQueuedAsyncEffect from '@joplin/lib/hooks/useQueuedAsyncEffect'; const logger = Logger.create('useFormNote'); @@ -67,6 +68,82 @@ function resourceInfosChanged(a: ResourceInfos, b: ResourceInfos): boolean { return false; } +// Should be a fast-to-compute string uniquely determined by properties of [note] that +// change the editor. +const computeNoteContentKey = (note: NoteEntity) => { + return JSON.stringify({ + id: note.id, + body: note.body, + title: note.title, + is_todo: note.is_todo, + todo_completed: note.todo_completed, + markup_language: note.markup_language, + encryption_applied: note.encryption_applied, + }); +}; + +type InitNoteStateCallback = (note: NoteEntity, isNew: boolean)=> Promise; +const useRefreshFormNoteOnChange = (formNoteRef: RefObject, noteId: string, initNoteState: InitNoteStateCallback) => { + // Increasing the value of this counter cancels any ongoing note refreshes and starts + // a new refresh. + const [formNoteRefreshScheduled, setFormNoteRefreshScheduled] = useState(0); + + useQueuedAsyncEffect(async (event) => { + if (formNoteRefreshScheduled <= 0) return; + if (formNoteRef.current.hasChanged) { + logger.info('Form note changed between scheduling a refresh and the refresh itself. Cancelling the refresh.'); + return; + } + + logger.info('Sync has finished and note has never been changed - reloading it'); + + const loadNote = async () => { + const n = await Note.load(noteId); + if (event.cancelled) return; + + // Normally should not happened because if the note has been deleted via sync + // it would not have been loaded in the editor (due to note selection changing + // on delete) + if (!n) { + logger.warn('Trying to reload note that has been deleted:', noteId); + return; + } + + await initNoteState(n, false); + if (event.cancelled) return; + setFormNoteRefreshScheduled(0); + }; + + await loadNote(); + }, [formNoteRefreshScheduled, noteId, initNoteState]); + + const refreshFormNote = useCallback(() => { + // Increase the counter to cancel any ongoing refresh attempts + // and start a new one. + setFormNoteRefreshScheduled(formNoteRefreshScheduled + 1); + }, [formNoteRefreshScheduled]); + + useEffect(() => { + if (!noteId) return ()=>{}; + + let cancelled = false; + + type ChangeEventSlice = { itemId: string }; + const listener = ({ itemId }: ChangeEventSlice) => { + if (itemId === noteId && !cancelled) { + if (formNoteRef.current.hasChanged) return; + refreshFormNote(); + } + }; + eventManager.on(EventName.ItemChange, listener); + + return () => { + eventManager.off(EventName.ItemChange, listener); + cancelled = true; + }; + }, [formNoteRef, noteId, refreshFormNote]); +}; + export default function useFormNote(dependencies: HookDependencies) { const { noteId, isProvisional, titleInputRef, editorRef, onBeforeLoad, onAfterLoad } = dependencies; @@ -77,12 +154,9 @@ export default function useFormNote(dependencies: HookDependencies) { const formNoteRef = useRef(formNote); formNoteRef.current = formNote; + const recentNoteKeys = useRef>(new Set()); - // Increasing the value of this counter cancels any ongoing note refreshes and starts - // a new refresh. - const [formNoteRefreshScheduled, setFormNoteRefreshScheduled] = useState(0); - - const initNoteState = useCallback(async (n: NoteEntity, isNewNote: boolean) => { + const initNoteState: InitNoteStateCallback = useCallback(async (n, isNewNote) => { let originalCss = ''; if (n.markup_language === MarkupToHtml.MARKUP_LANGUAGE_HTML) { @@ -110,6 +184,16 @@ export default function useFormNote(dependencies: HookDependencies) { logger.debug('Initializing note state'); + const newContentKey = computeNoteContentKey(newFormNote); + const changeIsFromRecentSave = recentNoteKeys.current.has(newContentKey); + recentNoteKeys.current.clear(); + recentNoteKeys.current.add(newContentKey); + + if (changeIsFromRecentSave) { + logger.debug('Cancelled note refresh -- the just-loaded note content was recently saved.'); + return null; + } + // Note that for performance reason,the call to setResourceInfos should // be first because it loads the resource infos in an async way. If we // swap them, the formNote will be updated first and rendered, then the @@ -122,9 +206,10 @@ export default function useFormNote(dependencies: HookDependencies) { logger.info('Cancelled note refresh -- form note changed while loading attached resources.'); return null; } - setResourceInfos(resources); setFormNote(newFormNote); + logger.debug('form note body change', formNoteRef.current.body, '->', newFormNote.body); + formNoteRef.current = newFormNote; logger.debug('Resource info and form note set.'); @@ -133,63 +218,7 @@ export default function useFormNote(dependencies: HookDependencies) { return newFormNote; }, []); - useEffect(() => { - if (formNoteRefreshScheduled <= 0) return () => {}; - if (formNoteRef.current.hasChanged) { - logger.info('Form note changed between scheduling a refresh and the refresh itself. Cancelling the refresh.'); - return () => {}; - } - - logger.info('Sync has finished and note has never been changed - reloading it'); - - let cancelled = false; - - const loadNote = async () => { - const n = await Note.load(noteId); - if (cancelled) return; - - // Normally should not happened because if the note has been deleted via sync - // it would not have been loaded in the editor (due to note selection changing - // on delete) - if (!n) { - logger.warn('Trying to reload note that has been deleted:', noteId); - return; - } - - await initNoteState(n, false); - - setFormNoteRefreshScheduled(0); - }; - - void loadNote(); - - return () => { - cancelled = true; - }; - }, [formNoteRefreshScheduled, noteId, initNoteState]); - - const refreshFormNote = useCallback(() => { - // Increase the counter to cancel any ongoing refresh attempts - // and start a new one. - setFormNoteRefreshScheduled(formNoteRefreshScheduled + 1); - }, [formNoteRefreshScheduled]); - - useEffect(() => { - if (!noteId) return ()=>{}; - - type ChangeEventSlice = { itemId: string }; - const listener = ({ itemId }: ChangeEventSlice) => { - if (itemId === noteId) { - if (formNoteRef.current.hasChanged) return; - refreshFormNote(); - } - }; - eventManager.on(EventName.ItemChange, listener); - - return () => { - eventManager.off(EventName.ItemChange, listener); - }; - }, [noteId, refreshFormNote]); + useRefreshFormNoteOnChange(formNoteRef, noteId, initNoteState); useEffect(() => { if (!noteId) { @@ -262,6 +291,7 @@ export default function useFormNote(dependencies: HookDependencies) { useEffect(() => { if (previousNoteId !== formNote.id) { void onResourceChange(); + recentNoteKeys.current.clear(); } }, [previousNoteId, formNote.id, onResourceChange]); @@ -287,14 +317,15 @@ export default function useFormNote(dependencies: HookDependencies) { // changes, with no delay during which async code can run. Even a small delay (e.g. that introduced // by a setState -> useEffect) can lead to a race condition. See https://github.com/laurent22/joplin/issues/8960. const onSetFormNote: OnSetFormNote = useCallback(newFormNote => { + let newNote; if (typeof newFormNote === 'function') { - const newNote = newFormNote(formNoteRef.current); - formNoteRef.current = newNote; - setFormNote(newNote); + newNote = newFormNote(formNoteRef.current); } else { - formNoteRef.current = newFormNote; - setFormNote(newFormNote); + newNote = newFormNote; } + recentNoteKeys.current.add(computeNoteContentKey(newNote)); + formNoteRef.current = newNote; + setFormNote(newNote); }, [setFormNote]); return { From a2680577085c93ebb5f6aaa1691c34c1d7b72be7 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 10 Oct 2024 09:33:24 -0700 Subject: [PATCH 12/97] Reduce debug logging --- packages/app-desktop/gui/NoteEditor/utils/useFormNote.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/app-desktop/gui/NoteEditor/utils/useFormNote.ts b/packages/app-desktop/gui/NoteEditor/utils/useFormNote.ts index c5319da8014..7b5c6393131 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/useFormNote.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/useFormNote.ts @@ -208,7 +208,6 @@ export default function useFormNote(dependencies: HookDependencies) { } setResourceInfos(resources); setFormNote(newFormNote); - logger.debug('form note body change', formNoteRef.current.body, '->', newFormNote.body); formNoteRef.current = newFormNote; logger.debug('Resource info and form note set.'); From 6184579792ec25ff02832fb452d7820a2c5626eb Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 10 Oct 2024 10:34:56 -0700 Subject: [PATCH 13/97] Fix tag dialog and styled components don't work in new windows --- .../gui/NoteEditor/NoteEditorWrapper.tsx | 45 ++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx b/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx index f1432d3b775..319e6b0148d 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useCallback } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import NoteEditor from './NoteEditor'; import StyleSheetContainer from '../StyleSheets/StyleSheetContainer'; import { connect } from 'react-redux'; @@ -8,6 +8,10 @@ import { Dispatch } from 'redux'; import NewWindowOrIFrame from '../NewWindowOrIFrame'; import WindowCommandHandler from '../WindowCommandHandler/WindowCommandHandler'; +import createCache from '@emotion/cache'; +import { CacheProvider } from '@emotion/react'; +const { StyleSheetManager } = require('styled-components'); + interface Props { dispatch: Dispatch; themeId: number; @@ -26,13 +30,40 @@ const NoteEditorWrapper: React.FC = props => { const noteId = props.noteId ?? props.defaultNoteId; const editor = ; - // TODO: Always render the editor in an ; + borderBottom={props.borderBottom} + >; } export default forwardRef(UserWebview); diff --git a/packages/app-desktop/services/plugins/UserWebviewDialog.tsx b/packages/app-desktop/services/plugins/UserWebviewDialog.tsx index 6c74eb419d5..942d63da456 100644 --- a/packages/app-desktop/services/plugins/UserWebviewDialog.tsx +++ b/packages/app-desktop/services/plugins/UserWebviewDialog.tsx @@ -7,12 +7,18 @@ import UserWebview, { Props as UserWebviewProps } from './UserWebview'; import UserWebviewDialogButtonBar from './UserWebviewDialogButtonBar'; import { focus } from '@joplin/lib/utils/focusHandler'; import Dialog from '../../gui/Dialog'; +const styled = require('styled-components').default; interface Props extends UserWebviewProps { buttons: ButtonSpec[]; fitToContent: boolean; } +const UserWebViewWrapper = styled.div` + display: flex; + flex: 1; +`; + function defaultButtons(): ButtonSpec[] { return [ { @@ -78,7 +84,7 @@ export default function UserWebviewDialog(props: Props) { return ( -
+ -
+
); diff --git a/packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.tsx b/packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.tsx index 6bd32190359..0dc646e1e5a 100644 --- a/packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.tsx +++ b/packages/app-desktop/services/plugins/UserWebviewDialogButtonBar.tsx @@ -5,10 +5,20 @@ import { ButtonSpec } from '@joplin/lib/services/plugins/api/types'; const styled = require('styled-components').default; const { space } = require('styled-system'); +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied +type StyleProps = any; + interface Props { buttons: ButtonSpec[]; } +const StyledRoot = styled.div` + display: flex; + width: 100%; + box-sizing: border-box; + justify-content: flex-end; + padding-top: ${(props: StyleProps) => props.theme.mainPadding}px; +`; const StyledButton = styled(Button)`${space}`; @@ -38,8 +48,8 @@ export default function UserWebviewDialogButtonBar(props: Props) { } return ( -
+ {renderButtons()} -
+
); } diff --git a/packages/app-desktop/services/plugins/styles/index.scss b/packages/app-desktop/services/plugins/styles/index.scss deleted file mode 100644 index fc2b1227093..00000000000 --- a/packages/app-desktop/services/plugins/styles/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@use './plugin-user-webview.scss'; -@use './user-dialog-wrapper.scss'; -@use './user-dialog-button-bar.scss'; diff --git a/packages/app-desktop/services/plugins/styles/plugin-user-webview.scss b/packages/app-desktop/services/plugins/styles/plugin-user-webview.scss deleted file mode 100644 index 43a8e732cbb..00000000000 --- a/packages/app-desktop/services/plugins/styles/plugin-user-webview.scss +++ /dev/null @@ -1,17 +0,0 @@ - -.plugin-user-webview { - padding: 0; - margin: 0; - border: none; - width: 100%; - height: 100%; - - &.-border-bottom { - border-bottom: 1px solid var(--joplin-divider-color); - } - - &.-fit-to-content { - width: var(--content-width); - height: var(--content-height); - } -} diff --git a/packages/app-desktop/services/plugins/styles/user-dialog-button-bar.scss b/packages/app-desktop/services/plugins/styles/user-dialog-button-bar.scss deleted file mode 100644 index 5e2dcfe44ed..00000000000 --- a/packages/app-desktop/services/plugins/styles/user-dialog-button-bar.scss +++ /dev/null @@ -1,8 +0,0 @@ - -.user-dialog-button-bar { - display: flex; - width: 100%; - box-sizing: border-box; - justify-content: flex-end; - padding-top: var(--joplin-main-padding); -} \ No newline at end of file diff --git a/packages/app-desktop/services/plugins/styles/user-dialog-wrapper.scss b/packages/app-desktop/services/plugins/styles/user-dialog-wrapper.scss deleted file mode 100644 index d001e28d746..00000000000 --- a/packages/app-desktop/services/plugins/styles/user-dialog-wrapper.scss +++ /dev/null @@ -1,5 +0,0 @@ - -.user-dialog-wrapper { - display: flex; - flex: 1; -} diff --git a/packages/app-desktop/style.scss b/packages/app-desktop/style.scss index 3765576fde2..de5b565c1b0 100644 --- a/packages/app-desktop/style.scss +++ b/packages/app-desktop/style.scss @@ -12,6 +12,5 @@ @use 'gui/TrashNotification/style.scss' as trash-notification; @use 'gui/Sidebar/style.scss' as sidebar-styles; @use 'gui/NoteEditor/style.scss' as note-editor-styles; -@use 'services/plugins/styles/index.scss' as plugins-styles; @use 'gui/styles/index.scss' as gui-styles; @use 'main.scss' as main; From b271333965ba80cb990c399e7a3a3ab556b52527 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 10 Oct 2024 11:43:13 -0700 Subject: [PATCH 15/97] Add note about @emotion/ transitive dependency --- packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx b/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx index 319e6b0148d..3c13201ada9 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx @@ -8,9 +8,10 @@ import { Dispatch } from 'redux'; import NewWindowOrIFrame from '../NewWindowOrIFrame'; import WindowCommandHandler from '../WindowCommandHandler/WindowCommandHandler'; +const { StyleSheetManager } = require('styled-components'); +// Note: Transitive dependencies used only by react-select. Remove if react-select is removed. import createCache from '@emotion/cache'; import { CacheProvider } from '@emotion/react'; -const { StyleSheetManager } = require('styled-components'); interface Props { dispatch: Dispatch; From f4404898e7b8d5fe9630e5cac730c2f1b961f043 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 11 Oct 2024 18:21:52 -0700 Subject: [PATCH 16/97] Fix GotoAnything has wrong height in secondary windows --- packages/app-desktop/plugins/GotoAnything.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/app-desktop/plugins/GotoAnything.tsx b/packages/app-desktop/plugins/GotoAnything.tsx index fdad2636b01..b96c76c5473 100644 --- a/packages/app-desktop/plugins/GotoAnything.tsx +++ b/packages/app-desktop/plugins/GotoAnything.tsx @@ -636,7 +636,9 @@ class DialogComponent extends React.PureComponent { } private calculateMaxHeight(itemHeight: number) { - const maxItemCount = Math.floor((0.7 * window.innerHeight) / itemHeight); + const listContainer: HTMLElement|null = this.itemListRef.current?.container; + const containerWindow = listContainer?.ownerDocument?.defaultView ?? window; + const maxItemCount = Math.floor((0.7 * containerWindow.innerHeight) / itemHeight); return maxItemCount * itemHeight; } From 2aa5576157a28f217b8a36e6782b61da31686427 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 11 Oct 2024 20:37:20 -0700 Subject: [PATCH 17/97] Fix links in new windows --- packages/app-desktop/ElectronAppWrapper.ts | 52 +++++++++++++--------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/packages/app-desktop/ElectronAppWrapper.ts b/packages/app-desktop/ElectronAppWrapper.ts index dc00124674d..360b1ff94fe 100644 --- a/packages/app-desktop/ElectronAppWrapper.ts +++ b/packages/app-desktop/ElectronAppWrapper.ts @@ -5,7 +5,7 @@ import type ShimType from '@joplin/lib/shim'; const shim: typeof ShimType = require('@joplin/lib/shim').default; import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils'; -import { BrowserWindow, Tray, screen } from 'electron'; +import { BrowserWindow, Tray, WebContents, screen } from 'electron'; import bridge from './bridge'; const url = require('url'); const path = require('path'); @@ -232,26 +232,38 @@ export default class ElectronAppWrapper { }, 3000); } - // will-frame-navigate is fired by clicking on a link within the BrowserWindow. - this.win_.webContents.on('will-frame-navigate', event => { - // If the link changes the URL of the browser window, - if (event.isMainFrame) { - event.preventDefault(); - void bridge().openExternal(event.url); - } - }); + const addWindowEventHandlers = (webContents: WebContents) => { + // will-frame-navigate is fired by clicking on a link within the BrowserWindow. + webContents.on('will-frame-navigate', event => { + // If the link changes the URL of the browser window, + if (event.isMainFrame) { + event.preventDefault(); + void bridge().openExternal(event.url); + } + }); - // Override calls to window.open and links with target="_blank": Open most in a browser instead - // of Electron: - this.win_.webContents.setWindowOpenHandler((event) => { - if (event.url === 'about:blank') { - // Script-controlled pages: Used for opening notes in new windows - return { action: 'allow', outlivesOpener: true }; - } else if (event.url.match(/^https?:\/\//)) { - void bridge().openExternal(event.url); - } - return { action: 'deny' }; - }); + // Override calls to window.open and links with target="_blank": Open most in a browser instead + // of Electron: + webContents.setWindowOpenHandler((event) => { + if (event.url === 'about:blank') { + // Script-controlled pages: Used for opening notes in new windows + return { + action: 'allow', + overrideBrowserWindowOptions: { + webPreferences: { }, + }, + }; + } else if (event.url.match(/^https?:\/\//)) { + void bridge().openExternal(event.url); + } + return { action: 'deny' }; + }); + + webContents.on('did-create-window', (event) => { + addWindowEventHandlers(event.webContents); + }); + }; + addWindowEventHandlers(this.win_.webContents); // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied this.win_.on('close', (event: any) => { From f7998f4004bc3dab7ededb4e409273955a8b7a8f Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 11 Oct 2024 22:23:24 -0700 Subject: [PATCH 18/97] Update reducer.ts to give different windows different selections --- packages/app-desktop/app.reducer.ts | 11 - packages/app-desktop/app.ts | 7 + .../commands/openNoteInNewWindow.ts | 11 +- packages/app-desktop/gui/MainScreen.tsx | 20 +- .../app-desktop/gui/NewWindowOrIFrame.tsx | 11 + .../gui/NoteEditor/NoteEditorWrapper.tsx | 41 ++- packages/app-desktop/gui/NoteTextViewer.tsx | 3 + packages/lib/BaseApplication.ts | 5 + packages/lib/reducer.ts | 309 ++++++++++++------ 9 files changed, 285 insertions(+), 133 deletions(-) diff --git a/packages/app-desktop/app.reducer.ts b/packages/app-desktop/app.reducer.ts index 30801201ecc..368a5908a37 100644 --- a/packages/app-desktop/app.reducer.ts +++ b/packages/app-desktop/app.reducer.ts @@ -69,7 +69,6 @@ export function createAppDefaultState(windowContentSize: any, resourceEditWatche noteVisiblePanes: ['editor', 'viewer'], windowContentSize, // bridge().windowContentSize(), watchedNoteFiles: [], - newWindowNoteIds: [], lastEditorScrollPercents: {}, devToolsVisible: false, visibleDialogs: {}, // empty object if no dialog is visible. Otherwise contains the list of visible dialogs. @@ -260,16 +259,6 @@ export default function(state: AppState, action: any) { } break; - case 'NOTE_WINDOW_OPEN': - if (!state.newWindowNoteIds.includes(action.noteId)) { - newState = { ...state, newWindowNoteIds: [...state.newWindowNoteIds, action.noteId] }; - } - break; - - case 'NOTE_WINDOW_CLOSE': - newState = { ...state, newWindowNoteIds: state.newWindowNoteIds.filter(id => id !== action.noteId) }; - break; - case 'EDITOR_SCROLL_PERCENT_SET': { diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index 0b39402e69e..77bf3847dc6 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -661,6 +661,13 @@ class Application extends BaseApplication { Setting.setValue('linking.extraAllowedExtensions', newExtensions); }); + window.addEventListener('focus', () => { + this.dispatch({ + type: 'WINDOW_FOCUS', + windowId: 'default', + }); + }); + await this.initPluginService(); this.setupContextMenu(); diff --git a/packages/app-desktop/commands/openNoteInNewWindow.ts b/packages/app-desktop/commands/openNoteInNewWindow.ts index dc6c8e2b9e2..47ede1f3330 100644 --- a/packages/app-desktop/commands/openNoteInNewWindow.ts +++ b/packages/app-desktop/commands/openNoteInNewWindow.ts @@ -1,6 +1,7 @@ import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService'; import { _ } from '@joplin/lib/locale'; import { stateUtils } from '@joplin/lib/reducer'; +import Note from '@joplin/lib/models/Note'; export const declaration: CommandDeclaration = { name: 'openNoteInNewWindow', @@ -8,11 +9,19 @@ export const declaration: CommandDeclaration = { iconName: 'icon-share', }; +let idCounter = 0; + export const runtime = (): CommandRuntime => { return { execute: async (context: CommandContext, noteId: string = null) => { noteId = noteId || stateUtils.selectedNoteId(context.state); - context.dispatch({ type: 'NOTE_WINDOW_OPEN', noteId }); + const note = await Note.load(noteId, { fields: ['parent_id'] }); + context.dispatch({ + type: 'WINDOW_OPEN', + noteId, + folderId: note.parent_id, + windowId: `window-${noteId}-${idCounter++}`, + }); }, enabledCondition: 'oneNoteSelected', }; diff --git a/packages/app-desktop/gui/MainScreen.tsx b/packages/app-desktop/gui/MainScreen.tsx index 06261426e21..580f29eafcd 100644 --- a/packages/app-desktop/gui/MainScreen.tsx +++ b/packages/app-desktop/gui/MainScreen.tsx @@ -10,7 +10,7 @@ import Sidebar from './Sidebar/Sidebar'; import UserWebview from '../services/plugins/UserWebview'; import UserWebviewDialog from '../services/plugins/UserWebviewDialog'; import { ContainerType } from '@joplin/lib/services/plugins/WebviewController'; -import { StateLastDeletion, stateUtils } from '@joplin/lib/reducer'; +import { StateLastDeletion, stateUtils, WindowState } from '@joplin/lib/reducer'; import { _ } from '@joplin/lib/locale'; import NoteListWrapper from './NoteListWrapper/NoteListWrapper'; import { AppState } from '../app.reducer'; @@ -52,7 +52,7 @@ interface Props { // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied dispatch: Function; mainLayout: LayoutItem; - newWindowNoteIds: string[]; + secondaryWindowStates: WindowState[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied style: any; layoutMoveMode: boolean; @@ -635,7 +635,11 @@ class MainScreenComponent extends React.Component { }, editor: () => { - return ; + return ; }, }; @@ -744,8 +748,12 @@ class MainScreenComponent extends React.Component { /> ) : null; - const newWindowNotes = this.props.newWindowNoteIds.map(noteId => { - return ; + const newWindowNotes = this.props.secondaryWindowStates.map(windowState => { + return ; }); return ( @@ -801,7 +809,7 @@ const mapStateToProps = (state: AppState) => { notesSortOrderReverse: state.settings['notes.sortOrder.reverse'], notesColumns: validateColumns(state.settings['notes.columns']), showInvalidJoplinCloudCredential: state.settings['sync.target'] === 10 && state.mustAuthenticate, - newWindowNoteIds: state.newWindowNoteIds, + secondaryWindowStates: stateUtils.secondaryWindowStates(state), }; }; diff --git a/packages/app-desktop/gui/NewWindowOrIFrame.tsx b/packages/app-desktop/gui/NewWindowOrIFrame.tsx index 4e8daea67aa..83949aca28c 100644 --- a/packages/app-desktop/gui/NewWindowOrIFrame.tsx +++ b/packages/app-desktop/gui/NewWindowOrIFrame.tsx @@ -9,6 +9,7 @@ interface Props { children: React.ReactNode[]|React.ReactNode; newWindow: boolean; onClose: ()=> void; + onFocus?: ()=> void; } const NewWindowOrIFrame: React.FC = props => { @@ -18,6 +19,8 @@ const NewWindowOrIFrame: React.FC = props => { const onCloseRef = useRef(props.onClose); onCloseRef.current = props.onClose; + const onFocusRef = useRef(props.onFocus); + onFocusRef.current = props.onFocus; useEffect(() => { let openedWindow: Window|null = null; @@ -87,6 +90,14 @@ const NewWindowOrIFrame: React.FC = props => { doc.body.style.height = '100vh'; + const containerWindow = doc.defaultView; + containerWindow.addEventListener('focus', () => { + onFocusRef.current?.(); + }); + if (doc.hasFocus()) { + onFocusRef.current?.(); + } + setLoaded(true); }, [doc]); const parentNode = loaded ? doc?.body : null; diff --git a/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx b/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx index 3c13201ada9..065304a77d7 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx @@ -12,33 +12,51 @@ const { StyleSheetManager } = require('styled-components'); // Note: Transitive dependencies used only by react-select. Remove if react-select is removed. import createCache from '@emotion/cache'; import { CacheProvider } from '@emotion/react'; +import { stateUtils } from '@joplin/lib/reducer'; interface Props { dispatch: Dispatch; themeId: number; bodyEditor: string; newWindow: boolean; - noteId?: string; - defaultNoteId: string; + windowId: string; + activeWindowId: string; + + secondaryWindowNoteIds: Record; provisionalNoteIds: string[]; } const NoteEditorWrapper: React.FC = props => { - const onClose = useCallback(() => { - props.dispatch({ type: 'NOTE_WINDOW_CLOSE', noteId: props.noteId }); - }, [props.dispatch, props.noteId]); + const newWindow = props.newWindow; + const onWindowClose = useCallback(() => { + if (newWindow) { + props.dispatch({ type: 'WINDOW_CLOSE', windowId: props.windowId }); + } + }, [props.dispatch, props.windowId, newWindow]); + + const onWindowFocus = useCallback(() => { + props.dispatch({ type: 'WINDOW_FOCUS', windowId: props.windowId }); + }, [props.dispatch, props.windowId]); - const noteId = props.noteId ?? props.defaultNoteId; - const editor = ; + const noteId = props.secondaryWindowNoteIds[props.windowId][0] ?? ''; + const editor = noteId ? : null; - const windowContent = + const windowContent = {editor} ; - return props.newWindow ? windowContent : editor; + return newWindow ? windowContent : editor; }; interface StyleProviderProps { @@ -76,12 +94,11 @@ export default connect((state: AppState) => { bodyEditor = 'CodeMirror5'; } - const defaultNoteId = state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null; - return { themeId: state.settings.theme, provisionalNoteIds: state.provisionalNoteIds, - defaultNoteId: defaultNoteId, + secondaryWindowNoteIds: stateUtils.windowIdToSelectedNoteIds(state), + activeWindowId: stateUtils.activeWindowId(state), bodyEditor, }; })(NoteEditorWrapper); diff --git a/packages/app-desktop/gui/NoteTextViewer.tsx b/packages/app-desktop/gui/NoteTextViewer.tsx index b212274334c..3c58540aac1 100644 --- a/packages/app-desktop/gui/NoteTextViewer.tsx +++ b/packages/app-desktop/gui/NoteTextViewer.tsx @@ -178,6 +178,9 @@ export default class NoteTextViewerComponent extends React.Component public send(channel: string, arg0: any = null, arg1: any = null) { const win = this.webviewRef_.current.contentWindow; + // Window may already be closed + if (!win) return; + if (channel === 'focus') { win.postMessage({ target: 'webview', name: 'focus', data: {} }, '*'); } diff --git a/packages/lib/BaseApplication.ts b/packages/lib/BaseApplication.ts index 2b830f332ea..53d748129db 100644 --- a/packages/lib/BaseApplication.ts +++ b/packages/lib/BaseApplication.ts @@ -511,6 +511,11 @@ export default class BaseApplication { refreshNotesUseSelectedNoteId = true; } + if (action.type === 'WINDOW_FOCUS') { + refreshNotes = true; + refreshNotesUseSelectedNoteId = true; + } + // Should refresh the notes when: // - A tag is selected, to show the notes for that tag // - When a tag is updated so that when searching by tags, the search results are updated diff --git a/packages/lib/reducer.ts b/packages/lib/reducer.ts index 0254aaa060f..726e0f92328 100644 --- a/packages/lib/reducer.ts +++ b/packages/lib/reducer.ts @@ -7,7 +7,7 @@ import BaseModel from './BaseModel'; import { Store } from 'redux'; import { ProfileConfig } from './services/profileConfig/types'; import * as ArrayUtils from './ArrayUtils'; -import { FolderEntity, NoteEntity } from './services/database/types'; +import { FolderEntity, NoteEntity, NoteTagEntity } from './services/database/types'; import { getListRendererIds } from './services/noteList/renderers'; import { ProcessResultsRow } from './services/search/SearchEngine'; import { getDisplayParentId } from './services/trash'; @@ -70,20 +70,15 @@ export interface StateLastDeletion { timestamp: number; } -export interface State { +export interface WindowState { + windowId: string; notes: NoteEntity[]; noteSelectionEnabled?: boolean; notesSource: string; notesParentType: string; - folders: FolderEntity[]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - tags: any[]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - masterKeys: any[]; - notLoadedMasterKeys: string[]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - searches: any[]; - highlightedWords: string[]; + selectedNoteTags: NoteTagEntity[]; + searchQuery: string; + selectedNoteIds: string[]; selectedNoteHash: string; selectedFolderId: string; @@ -91,6 +86,40 @@ export interface State { selectedSearchId: string; selectedItemType: string; selectedSmartFilterId: string; + + backwardHistoryNotes: NoteEntity[]; + forwardHistoryNotes: NoteEntity[]; +} + +const defaultWindowState: WindowState = { + windowId: 'default', + searchQuery: '', + notes: [], + notesSource: '', + notesParentType: null, + selectedNoteIds: [], + selectedNoteHash: '', + selectedFolderId: null, + selectedTagId: null, + selectedSearchId: null, + selectedSmartFilterId: null, + selectedItemType: 'note', + selectedNoteTags: [], + backwardHistoryNotes: [], + forwardHistoryNotes: [], +}; + +export interface State extends WindowState { + backgroundWindows: Record; + + folders: FolderEntity[]; + tags: NoteTagEntity[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + masterKeys: any[]; + notLoadedMasterKeys: string[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + searches: any[]; + highlightedWords: string[]; lastSelectedNotesIds: StateLastSelectedNotesIds; showSideMenu: boolean; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied @@ -99,7 +128,6 @@ export interface State { syncStarted: boolean; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied syncReport: any; - searchQuery: string; searchResults: ProcessResultsRow[]; settings: Partial; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied @@ -112,14 +140,8 @@ export interface State { collapsedFolderIds: string[]; clipperServer: StateClipperServer; decryptionWorker: StateDecryptionWorker; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - selectedNoteTags: any[]; resourceFetcher: StateResourceFetcher; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - backwardHistoryNotes: any[]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - forwardHistoryNotes: any[]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied pluginsLegacy: any; provisionalNoteIds: string[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied @@ -141,22 +163,14 @@ export interface State { } export const defaultState: State = { - notes: [], - notesSource: '', - notesParentType: null, + ...defaultWindowState, + backgroundWindows: {}, folders: [], tags: [], masterKeys: [], notLoadedMasterKeys: [], searches: [], highlightedWords: [], - selectedNoteIds: [], - selectedNoteHash: '', - selectedFolderId: null, - selectedTagId: null, - selectedSearchId: null, - selectedSmartFilterId: null, - selectedItemType: 'note', lastSelectedNotesIds: { Folder: {}, Tag: {}, @@ -189,13 +203,10 @@ export const defaultState: State = { decryptedItemCount: 0, skippedItemCount: 0, }, - selectedNoteTags: [], resourceFetcher: { toFetchCount: 0, fetchingCount: 0, }, - backwardHistoryNotes: [], - forwardHistoryNotes: [], // pluginsLegacy is the original plugin system, which eventually was used only for GotoAnything. // GotoAnything should be refactored to part of core and when it's done the pluginsLegacy key can // be removed. It was originally named "plugins", then renamed "pluginsLegacy" so as not to conflict @@ -290,7 +301,7 @@ class StateUtils { return selectArrayShallow(props, cacheKey); } - public oneNoteSelected(state: State): boolean { + public oneNoteSelected(state: WindowState): boolean { return state.selectedNoteIds.length === 1; } @@ -352,17 +363,45 @@ class StateUtils { return output ? output : []; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - public selectedNote(state: State): any { + public selectedNote(state: WindowState): NoteEntity { const noteId = this.selectedNoteId(state); return noteId ? BaseModel.byId(state.notes, noteId) : null; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - public selectedNoteId(state: State): any { + public selectedNoteId(state: WindowState): string|null { return state.selectedNoteIds.length ? state.selectedNoteIds[0] : null; } + public activeWindowId(state: State) { + return state.windowId; + } + + private allWindowIds(state: State) { + return [state.windowId, ...Object.keys(state.backgroundWindows)]; + } + + public windowStateById(state: State, id: string) { + return id === state.windowId ? state : state.backgroundWindows[id]; + } + + public mainWindowState(state: State) { + return this.windowStateById(state, 'default'); + } + + public secondaryWindowStates(state: State) { + const windowIds = [state.windowId, ...Object.keys(state.backgroundWindows)]; + return windowIds + .filter(id => (id !== 'default')) + .map(id => this.windowStateById(state, id)); + } + + public windowIdToSelectedNoteIds(state: State) { + const result: Record = {}; + for (const id of this.allWindowIds(state)) { + result[id] = this.windowStateById(state, id).selectedNoteIds; + } + return result; + } } export const stateUtils: StateUtils = new StateUtils(); @@ -815,6 +854,61 @@ function handleHistory(draft: Draft, action: any) { } } +type WindowAction = { + type: 'WINDOW_OPEN'; + windowId: string; + folderId: string; + noteId: string; +}|{ + type: 'WINDOW_FOCUS'|'WINDOW_CLOSE'; + windowId: string; +}; + +const handleWindowActions = (draft: Draft, action: WindowAction) => { + switch (action.type) { + + case 'WINDOW_OPEN': { + if (action.windowId in draft.backgroundWindows) { + throw new Error(`Window with id ${action.windowId} is already open!`); + } + + draft.backgroundWindows[action.windowId] = { + ...defaultWindowState, + notesParentType: 'Folder', + selectedFolderId: action.folderId, + windowId: action.windowId, + selectedNoteIds: [action.noteId], + }; + break; + } + case 'WINDOW_FOCUS': { + if (draft.windowId !== action.windowId) { + const windowId = action.windowId; + const previousWindowId = draft.windowId; + + const focusedWindowState = stateUtils.windowStateById(draft, windowId); + const previousWindowState = { ...defaultWindowState }; + + for (const key of Object.keys(defaultWindowState)) { + const stateKey = key as keyof WindowState; + + type AssignableWindowState = Record; + (previousWindowState as AssignableWindowState)[stateKey] = draft[stateKey]; + (draft as AssignableWindowState)[stateKey] = focusedWindowState[stateKey]; + } + + delete draft.backgroundWindows[windowId]; + draft.backgroundWindows[previousWindowId] = previousWindowState; + } + break; + } + case 'WINDOW_CLOSE': { + delete draft.backgroundWindows[action.windowId]; + break; + } + } +}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied const reducer = produce((draft: Draft = defaultState, action: any) => { @@ -829,6 +923,8 @@ const reducer = produce((draft: Draft = defaultState, action: any) => { handleHistory(draft, action); } + handleWindowActions(draft, action); + try { switch (action.type) { @@ -952,87 +1048,94 @@ const reducer = produce((draft: Draft = defaultState, action: any) => { case 'NOTE_UPDATE_ONE': { const modNote: NoteEntity = action.note; - const isViewingAllNotes = (draft.notesParentType === 'SmartFilter' && draft.selectedSmartFilterId === ALL_NOTES_FILTER_ID); - const isViewingConflictFolder = draft.notesParentType === 'Folder' && draft.selectedFolderId === Folder.conflictFolderId(); - - const noteIsInFolder = function(note: NoteEntity, folderId: string) { - if (note.is_conflict && isViewingConflictFolder) return true; - const noteDisplayParentId = getDisplayParentId(note, draft.folders.find(f => f.id === note.parent_id)); - return folderId === noteDisplayParentId; - }; - - let movedNotePreviousIndex = 0; - let noteFolderHasChanged = false; - const newNotes = draft.notes.slice(); - let found = false; - for (let i = 0; i < newNotes.length; i++) { - const n = newNotes[i]; - if (n.id === modNote.id) { - const previousDisplayParentId = ('parent_id' in n) ? getDisplayParentId(n, draft.folders.find(f => f.id === n.parent_id)) : ''; - if (n.is_conflict && !modNote.is_conflict) { - // Note was a conflict but was moved outside of - // the conflict folder - newNotes.splice(i, 1); - noteFolderHasChanged = true; - movedNotePreviousIndex = i; - } else if (isViewingAllNotes || noteIsInFolder(modNote, previousDisplayParentId)) { - // Note is still in the same folder - // Merge the properties that have changed (in modNote) into - // the object we already have. - newNotes[i] = { ...newNotes[i] }; - - for (const n in modNote) { - if (!modNote.hasOwnProperty(n)) continue; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - (newNotes[i] as any)[n] = (modNote as any)[n]; + const handleWindowState = (windowDraft: Draft) => { + const isViewingAllNotes = (windowDraft.notesParentType === 'SmartFilter' && windowDraft.selectedSmartFilterId === ALL_NOTES_FILTER_ID); + const isViewingConflictFolder = windowDraft.notesParentType === 'Folder' && windowDraft.selectedFolderId === Folder.conflictFolderId(); + + const noteIsInFolder = function(note: NoteEntity, folderId: string) { + if (note.is_conflict && isViewingConflictFolder) return true; + const noteDisplayParentId = getDisplayParentId(note, draft.folders.find(f => f.id === note.parent_id)); + return folderId === noteDisplayParentId; + }; + + let movedNotePreviousIndex = 0; + let noteFolderHasChanged = false; + const newNotes = windowDraft.notes.slice(); + let found = false; + for (let i = 0; i < newNotes.length; i++) { + const n = newNotes[i]; + if (n.id === modNote.id) { + const previousDisplayParentId = ('parent_id' in n) ? getDisplayParentId(n, draft.folders.find(f => f.id === n.parent_id)) : ''; + if (n.is_conflict && !modNote.is_conflict) { + // Note was a conflict but was moved outside of + // the conflict folder + newNotes.splice(i, 1); + noteFolderHasChanged = true; + movedNotePreviousIndex = i; + } else if (isViewingAllNotes || noteIsInFolder(modNote, previousDisplayParentId)) { + // Note is still in the same folder + // Merge the properties that have changed (in modNote) into + // the object we already have. + newNotes[i] = { ...newNotes[i] }; + + for (const n in modNote) { + if (!modNote.hasOwnProperty(n)) continue; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + (newNotes[i] as any)[n] = (modNote as any)[n]; + } + } else { + // Note has moved to a different folder + newNotes.splice(i, 1); + noteFolderHasChanged = true; + movedNotePreviousIndex = i; } - } else { - // Note has moved to a different folder - newNotes.splice(i, 1); - noteFolderHasChanged = true; - movedNotePreviousIndex = i; + found = true; + break; } - found = true; - break; } - } - // Note was not found - if the current folder is the same as the note folder, - // add it to it. - if (!found) { - if (isViewingAllNotes || noteIsInFolder(modNote, draft.selectedFolderId)) { - newNotes.push(modNote); + // Note was not found - if the current folder is the same as the note folder, + // add it to it. + if (!found) { + if (isViewingAllNotes || noteIsInFolder(modNote, windowDraft.selectedFolderId)) { + newNotes.push(modNote); + } } - } - draft.notes = newNotes; - - // Ensure that the selected note is still in the current folder. - // For example, if the user drags the current note to a different folder, - // a new note should be selected. - // In some cases, however, the selection needs to be preserved (e.g. the mobile app). - if (noteFolderHasChanged && !action.preserveSelection) { - let newIndex = movedNotePreviousIndex; - if (newIndex >= newNotes.length) newIndex = newNotes.length - 1; - if (!newNotes.length) newIndex = -1; - draft.selectedNoteIds = newIndex >= 0 ? [newNotes[newIndex].id] : []; - } + windowDraft.notes = newNotes; + + // Ensure that the selected note is still in the current folder. + // For example, if the user drags the current note to a different folder, + // a new note should be selected. + // In some cases, however, the selection needs to be preserved (e.g. the mobile app). + if (noteFolderHasChanged && !action.preserveSelection) { + let newIndex = movedNotePreviousIndex; + if (newIndex >= newNotes.length) newIndex = newNotes.length - 1; + if (!newNotes.length) newIndex = -1; + windowDraft.selectedNoteIds = newIndex >= 0 ? [newNotes[newIndex].id] : []; + } - if (!action.ignoreProvisionalFlag) { - let newProvisionalNoteIds = draft.provisionalNoteIds; + if (!action.ignoreProvisionalFlag) { + let newProvisionalNoteIds = draft.provisionalNoteIds; - if (action.provisional) { - newProvisionalNoteIds = newProvisionalNoteIds.slice(); - newProvisionalNoteIds.push(modNote.id); - } else { const idx = newProvisionalNoteIds.indexOf(modNote.id); - if (idx >= 0) { + if (action.provisional) { + if (idx < 0) { + newProvisionalNoteIds = newProvisionalNoteIds.slice(); + newProvisionalNoteIds.push(modNote.id); + } + } else if (idx >= 0) { newProvisionalNoteIds = newProvisionalNoteIds.slice(); newProvisionalNoteIds.splice(idx, 1); } + + draft.provisionalNoteIds = newProvisionalNoteIds; } + }; - draft.provisionalNoteIds = newProvisionalNoteIds; + handleWindowState(draft); + for (const backgroundWindow of Object.values(draft.backgroundWindows)) { + handleWindowState(backgroundWindow); } } break; From 55ca8533ef7928c25339467d3fcabaa41e807133 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 11 Oct 2024 22:29:37 -0700 Subject: [PATCH 19/97] Always use the data from the main window for the sidebar and note list --- packages/app-desktop/gui/NoteList/NoteList2.tsx | 10 ++++++---- packages/app-desktop/gui/Sidebar/FolderAndTagList.tsx | 11 +++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/app-desktop/gui/NoteList/NoteList2.tsx b/packages/app-desktop/gui/NoteList/NoteList2.tsx index 7c47bb25517..80b0a52285b 100644 --- a/packages/app-desktop/gui/NoteList/NoteList2.tsx +++ b/packages/app-desktop/gui/NoteList/NoteList2.tsx @@ -27,6 +27,7 @@ import { _ } from '@joplin/lib/locale'; import useActiveDescendantId from './utils/useActiveDescendantId'; import getNoteElementIdFromJoplinId from '../NoteListItem/utils/getNoteElementIdFromJoplinId'; import useFocusVisible from './utils/useFocusVisible'; +import { stateUtils } from '@joplin/lib/reducer'; const { connect } = require('react-redux'); const commands = { @@ -314,16 +315,17 @@ const NoteList = (props: Props) => { const mapStateToProps = (state: AppState) => { const selectedFolder: FolderEntity = state.notesParentType === 'Folder' ? Folder.byId(state.folders, state.selectedFolderId) : null; const userId = state.settings['sync.userId']; + const mainWindowState = stateUtils.mainWindowState(state); return { - notes: state.notes, + notes: mainWindowState.notes, folders: state.folders, - selectedNoteIds: state.selectedNoteIds, - selectedFolderId: state.selectedFolderId, + selectedNoteIds: mainWindowState.selectedNoteIds, + selectedFolderId: mainWindowState.selectedFolderId, themeId: state.settings.theme, notesParentType: state.notesParentType, searches: state.searches, - selectedSearchId: state.selectedSearchId, + selectedSearchId: mainWindowState.selectedSearchId, watchedNoteFiles: state.watchedNoteFiles, provisionalNoteIds: state.provisionalNoteIds, isInsertingNotes: state.isInsertingNotes, diff --git a/packages/app-desktop/gui/Sidebar/FolderAndTagList.tsx b/packages/app-desktop/gui/Sidebar/FolderAndTagList.tsx index 85bb4606981..804feaaad0f 100644 --- a/packages/app-desktop/gui/Sidebar/FolderAndTagList.tsx +++ b/packages/app-desktop/gui/Sidebar/FolderAndTagList.tsx @@ -14,6 +14,7 @@ import useFocusHandler from './hooks/useFocusHandler'; import useOnRenderItem from './hooks/useOnRenderItem'; import { ListItem } from './types'; import useSidebarCommandHandler from './hooks/useSidebarCommandHandler'; +import { stateUtils } from '@joplin/lib/reducer'; interface Props { dispatch: Dispatch; @@ -83,15 +84,17 @@ const FolderAndTagList: React.FC = props => { }; const mapStateToProps = (state: AppState) => { + const mainWindowState = stateUtils.mainWindowState(state); + return { themeId: state.settings.theme, tags: state.tags, folders: state.folders, - notesParentType: state.notesParentType, - selectedFolderId: state.selectedFolderId, - selectedTagId: state.selectedTagId, + notesParentType: mainWindowState.notesParentType, + selectedFolderId: mainWindowState.selectedFolderId, + selectedTagId: mainWindowState.selectedTagId, collapsedFolderIds: state.collapsedFolderIds, - selectedSmartFilterId: state.selectedSmartFilterId, + selectedSmartFilterId: mainWindowState.selectedSmartFilterId, plugins: state.pluginService.plugins, tagHeaderIsExpanded: state.settings.tagHeaderIsExpanded, folderHeaderIsExpanded: state.settings.folderHeaderIsExpanded, From 67d36decfb814e05e3bb911a204a4ca523dac579 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Mon, 14 Oct 2024 09:23:42 -0700 Subject: [PATCH 20/97] Fix secondary windows close when opening settings --- packages/app-desktop/app.reducer.ts | 1 - packages/app-desktop/gui/MainScreen.tsx | 13 +------------ packages/app-desktop/gui/Root.tsx | 15 +++++++++++++++ 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/app-desktop/app.reducer.ts b/packages/app-desktop/app.reducer.ts index 368a5908a37..858f1e9fd8d 100644 --- a/packages/app-desktop/app.reducer.ts +++ b/packages/app-desktop/app.reducer.ts @@ -38,7 +38,6 @@ export interface AppState extends State { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied windowContentSize: any; watchedNoteFiles: string[]; - newWindowNoteIds: string[]; lastEditorScrollPercents: EditorScrollPercents; devToolsVisible: boolean; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied diff --git a/packages/app-desktop/gui/MainScreen.tsx b/packages/app-desktop/gui/MainScreen.tsx index 580f29eafcd..605598c955d 100644 --- a/packages/app-desktop/gui/MainScreen.tsx +++ b/packages/app-desktop/gui/MainScreen.tsx @@ -10,7 +10,7 @@ import Sidebar from './Sidebar/Sidebar'; import UserWebview from '../services/plugins/UserWebview'; import UserWebviewDialog from '../services/plugins/UserWebviewDialog'; import { ContainerType } from '@joplin/lib/services/plugins/WebviewController'; -import { StateLastDeletion, stateUtils, WindowState } from '@joplin/lib/reducer'; +import { StateLastDeletion, stateUtils } from '@joplin/lib/reducer'; import { _ } from '@joplin/lib/locale'; import NoteListWrapper from './NoteListWrapper/NoteListWrapper'; import { AppState } from '../app.reducer'; @@ -52,7 +52,6 @@ interface Props { // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied dispatch: Function; mainLayout: LayoutItem; - secondaryWindowStates: WindowState[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied style: any; layoutMoveMode: boolean; @@ -748,14 +747,6 @@ class MainScreenComponent extends React.Component { /> ) : null; - const newWindowNotes = this.props.secondaryWindowStates.map(windowState => { - return ; - }); - return (
{ {messageComp} {layoutComp} - {newWindowNotes}
); } @@ -809,7 +799,6 @@ const mapStateToProps = (state: AppState) => { notesSortOrderReverse: state.settings['notes.sortOrder.reverse'], notesColumns: validateColumns(state.settings['notes.columns']), showInvalidJoplinCloudCredential: state.settings['sync.target'] === 10 && state.mustAuthenticate, - secondaryWindowStates: stateUtils.secondaryWindowStates(state), }; }; diff --git a/packages/app-desktop/gui/Root.tsx b/packages/app-desktop/gui/Root.tsx index 6ba602309a5..26bbad211a9 100644 --- a/packages/app-desktop/gui/Root.tsx +++ b/packages/app-desktop/gui/Root.tsx @@ -30,6 +30,8 @@ import Navigator from './Navigator'; import WelcomeUtils from '@joplin/lib/WelcomeUtils'; import JoplinCloudLoginScreen from './JoplinCloudLoginScreen'; import WindowCommandHandler from './WindowCommandHandler/WindowCommandHandler'; +import NoteEditorWrapper from './NoteEditor/NoteEditorWrapper'; +import { stateUtils, WindowState } from '@joplin/lib/reducer'; const { ThemeProvider, StyleSheetManager, createGlobalStyle } = require('styled-components'); const bridge = require('@electron/remote').require('./bridge').default; @@ -42,6 +44,7 @@ interface Props { zoomFactor: number; needApiAuth: boolean; dialogs: AppStateDialog[]; + secondaryWindowStates: WindowState[]; } interface ModalDialogProps { @@ -216,6 +219,16 @@ class RootComponent extends React.Component { return output; } + private renderSecondaryWindows() { + return this.props.secondaryWindowStates.map((windowState: WindowState) => { + return ; + }); + } + public render() { const navigatorStyle = { width: this.props.size.width / this.props.zoomFactor, @@ -243,6 +256,7 @@ class RootComponent extends React.Component { + {this.renderSecondaryWindows()} {this.renderModalMessage(this.modalDialogProps())} {this.renderDialogs()} @@ -260,6 +274,7 @@ const mapStateToProps = (state: AppState) => { needApiAuth: state.needApiAuth, dialogs: state.dialogs, profileConfigCurrentProfileId: state.profileConfig.currentProfileId, + secondaryWindowStates: stateUtils.secondaryWindowStates(state), }; }; From a1c6afa7eaf1552b1cd48b901af73135dfdc21fa Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Mon, 14 Oct 2024 11:06:56 -0700 Subject: [PATCH 21/97] WIP: Allow different windows to show different editors This commit converts the toggleEditors command to an editor command. --- .eslintignore | 5 +- .gitignore | 5 +- packages/app-desktop/gui/MainScreen.tsx | 2 +- .../app-desktop/gui/NoteEditor/NoteEditor.tsx | 744 +++--------------- .../gui/NoteEditor/NoteEditorContent.tsx | 656 +++++++++++++++ .../gui/NoteEditor/NoteEditorWrapper.tsx | 104 --- .../gui/NoteEditor/commands/index.ts | 2 + .../commands/toggleEditors.ts | 8 +- .../app-desktop/gui/NoteEditor/style.scss | 1 + .../styles/note-editor-wrapper.scss | 8 + .../utils/getWindowCommandPriority.ts | 15 + .../utils/useWindowCommandHandler.ts | 14 +- packages/app-desktop/gui/Root.tsx | 2 +- .../WindowCommandHandler/commands/index.ts | 2 - .../services/commands/ToolbarButtonUtils.ts | 4 +- 15 files changed, 821 insertions(+), 751 deletions(-) create mode 100644 packages/app-desktop/gui/NoteEditor/NoteEditorContent.tsx delete mode 100644 packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx rename packages/app-desktop/gui/{WindowCommandHandler => NoteEditor}/commands/toggleEditors.ts (77%) create mode 100644 packages/app-desktop/gui/NoteEditor/styles/note-editor-wrapper.scss create mode 100644 packages/app-desktop/gui/NoteEditor/utils/getWindowCommandPriority.ts diff --git a/.eslintignore b/.eslintignore index b8b1388651a..06b147a84fb 100644 --- a/.eslintignore +++ b/.eslintignore @@ -256,7 +256,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTabIndenter.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js packages/app-desktop/gui/NoteEditor/NoteEditor.js -packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.js +packages/app-desktop/gui/NoteEditor/NoteEditorContent.js packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js packages/app-desktop/gui/NoteEditor/WarningBanner/BannerContent.js packages/app-desktop/gui/NoteEditor/WarningBanner/WarningBanner.js @@ -266,6 +266,7 @@ packages/app-desktop/gui/NoteEditor/commands/index.js packages/app-desktop/gui/NoteEditor/commands/pasteAsText.js packages/app-desktop/gui/NoteEditor/commands/showLocalSearch.js packages/app-desktop/gui/NoteEditor/commands/showRevisions.js +packages/app-desktop/gui/NoteEditor/commands/toggleEditors.js packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.test.js packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.js packages/app-desktop/gui/NoteEditor/styles/index.js @@ -273,6 +274,7 @@ packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.test.js packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.js packages/app-desktop/gui/NoteEditor/utils/contextMenu.js packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.js +packages/app-desktop/gui/NoteEditor/utils/getWindowCommandPriority.js packages/app-desktop/gui/NoteEditor/utils/index.js packages/app-desktop/gui/NoteEditor/utils/markupRenderOptions.js packages/app-desktop/gui/NoteEditor/utils/resourceHandling.test.js @@ -440,7 +442,6 @@ packages/app-desktop/gui/WindowCommandHandler/commands/showShareFolderDialog.js packages/app-desktop/gui/WindowCommandHandler/commands/showShareNoteDialog.js packages/app-desktop/gui/WindowCommandHandler/commands/showSpellCheckerMenu.test.js packages/app-desktop/gui/WindowCommandHandler/commands/showSpellCheckerMenu.js -packages/app-desktop/gui/WindowCommandHandler/commands/toggleEditors.js packages/app-desktop/gui/WindowCommandHandler/commands/toggleLayoutMoveMode.js packages/app-desktop/gui/WindowCommandHandler/commands/toggleMenuBar.js packages/app-desktop/gui/WindowCommandHandler/commands/toggleNoteList.js diff --git a/.gitignore b/.gitignore index ac5fa1d693f..214adc89885 100644 --- a/.gitignore +++ b/.gitignore @@ -233,7 +233,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTabIndenter.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js packages/app-desktop/gui/NoteEditor/NoteEditor.js -packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.js +packages/app-desktop/gui/NoteEditor/NoteEditorContent.js packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js packages/app-desktop/gui/NoteEditor/WarningBanner/BannerContent.js packages/app-desktop/gui/NoteEditor/WarningBanner/WarningBanner.js @@ -243,6 +243,7 @@ packages/app-desktop/gui/NoteEditor/commands/index.js packages/app-desktop/gui/NoteEditor/commands/pasteAsText.js packages/app-desktop/gui/NoteEditor/commands/showLocalSearch.js packages/app-desktop/gui/NoteEditor/commands/showRevisions.js +packages/app-desktop/gui/NoteEditor/commands/toggleEditors.js packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.test.js packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.js packages/app-desktop/gui/NoteEditor/styles/index.js @@ -250,6 +251,7 @@ packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.test.js packages/app-desktop/gui/NoteEditor/utils/clipboardUtils.js packages/app-desktop/gui/NoteEditor/utils/contextMenu.js packages/app-desktop/gui/NoteEditor/utils/contextMenuUtils.js +packages/app-desktop/gui/NoteEditor/utils/getWindowCommandPriority.js packages/app-desktop/gui/NoteEditor/utils/index.js packages/app-desktop/gui/NoteEditor/utils/markupRenderOptions.js packages/app-desktop/gui/NoteEditor/utils/resourceHandling.test.js @@ -417,7 +419,6 @@ packages/app-desktop/gui/WindowCommandHandler/commands/showShareFolderDialog.js packages/app-desktop/gui/WindowCommandHandler/commands/showShareNoteDialog.js packages/app-desktop/gui/WindowCommandHandler/commands/showSpellCheckerMenu.test.js packages/app-desktop/gui/WindowCommandHandler/commands/showSpellCheckerMenu.js -packages/app-desktop/gui/WindowCommandHandler/commands/toggleEditors.js packages/app-desktop/gui/WindowCommandHandler/commands/toggleLayoutMoveMode.js packages/app-desktop/gui/WindowCommandHandler/commands/toggleMenuBar.js packages/app-desktop/gui/WindowCommandHandler/commands/toggleNoteList.js diff --git a/packages/app-desktop/gui/MainScreen.tsx b/packages/app-desktop/gui/MainScreen.tsx index 605598c955d..a6075d1e607 100644 --- a/packages/app-desktop/gui/MainScreen.tsx +++ b/packages/app-desktop/gui/MainScreen.tsx @@ -40,7 +40,7 @@ import { NoteListColumns } from '@joplin/lib/services/plugins/api/noteListType'; import validateColumns from './NoteListHeader/utils/validateColumns'; import TrashNotification from './TrashNotification/TrashNotification'; import UpdateNotification from './UpdateNotification/UpdateNotification'; -import NoteEditorWrapper from './NoteEditor/NoteEditorWrapper'; +import NoteEditorWrapper from './NoteEditor/NoteEditor'; const ipcRenderer = require('electron').ipcRenderer; diff --git a/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx b/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx index dcd2ef583f2..92fe0985b9e 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx @@ -1,656 +1,154 @@ import * as React from 'react'; -import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; -import TinyMCE from './NoteBody/TinyMCE/TinyMCE'; +import { RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import NoteEditorContent from './NoteEditorContent'; +import StyleSheetContainer from '../StyleSheets/StyleSheetContainer'; import { connect } from 'react-redux'; -import MultiNoteActions from '../MultiNoteActions'; -import { htmlToMarkdown } from './utils'; -import useSearchMarkers from './utils/useSearchMarkers'; -import useNoteSearchBar from './utils/useNoteSearchBar'; -import useMessageHandler from './utils/useMessageHandler'; -import useWindowCommandHandler from './utils/useWindowCommandHandler'; -import useDropHandler from './utils/useDropHandler'; -import useMarkupToHtml from './utils/useMarkupToHtml'; -import useFormNote, { OnLoadEvent, OnSetFormNote } from './utils/useFormNote'; -import useEffectiveNoteId from './utils/useEffectiveNoteId'; -import useFolder from './utils/useFolder'; -import styles_ from './styles'; -import { NoteEditorProps, FormNote, OnChangeEvent, NoteBodyEditorProps, AllAssetsOptions, NoteBodyEditorRef } from './utils/types'; -import CommandService from '@joplin/lib/services/CommandService'; -import ToolbarButton from '../ToolbarButton/ToolbarButton'; -import Button, { ButtonLevel } from '../Button/Button'; -import eventManager, { EventName } from '@joplin/lib/eventManager'; import { AppState } from '../../app.reducer'; -import ToolbarButtonUtils from '@joplin/lib/services/commands/ToolbarButtonUtils'; -import { _, _n } from '@joplin/lib/locale'; -import TagList from '../TagList'; -import NoteTitleBar from './NoteTitle/NoteTitleBar'; -import markupLanguageUtils from '../../utils/markupLanguageUtils'; -import Setting from '@joplin/lib/models/Setting'; -import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext'; -import ExternalEditWatcher from '@joplin/lib/services/ExternalEditWatcher'; -import { itemIsReadOnly } from '@joplin/lib/models/utils/readOnly'; -const { themeStyle } = require('@joplin/lib/theme'); -const { substrWithEllipsis } = require('@joplin/lib/string-utils'); -import NoteSearchBar from '../NoteSearchBar'; -import { reg } from '@joplin/lib/registry'; -import Note from '@joplin/lib/models/Note'; -import Folder from '@joplin/lib/models/Folder'; -import NoteRevisionViewer from '../NoteRevisionViewer'; -import { parseShareCache } from '@joplin/lib/services/share/reducer'; -import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect'; -import { ModelType } from '@joplin/lib/BaseModel'; -import BaseItem from '@joplin/lib/models/BaseItem'; -import { ErrorCode } from '@joplin/lib/errors'; -import ItemChange from '@joplin/lib/models/ItemChange'; -import PlainEditor from './NoteBody/PlainEditor/PlainEditor'; -import CodeMirror6 from './NoteBody/CodeMirror/v6/CodeMirror'; -import CodeMirror5 from './NoteBody/CodeMirror/v5/CodeMirror'; -import { openItemById } from './utils/contextMenu'; -import getPluginSettingValue from '@joplin/lib/services/plugins/utils/getPluginSettingValue'; -import { MarkupLanguage } from '@joplin/renderer'; -import useScrollWhenReadyOptions from './utils/useScrollWhenReadyOptions'; -import useScheduleSaveCallbacks from './utils/useScheduleSaveCallbacks'; -import WarningBanner from './WarningBanner/WarningBanner'; -const debounce = require('debounce'); - -const commands = [ - require('./commands/showRevisions'), -]; - -const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance()); - -function NoteEditor(props: NoteEditorProps) { - const [showRevisions, setShowRevisions] = useState(false); - const [titleHasBeenManuallyChanged, setTitleHasBeenManuallyChanged] = useState(false); - const [isReadOnly, setIsReadOnly] = useState(false); - - const editorRef = useRef(); - const titleInputRef = useRef(); - const isMountedRef = useRef(true); - const noteSearchBarRef = useRef(null); - - const setFormNoteRef = useRef(); - const { saveNoteIfWillChange, scheduleSaveNote } = useScheduleSaveCallbacks({ - setFormNote: setFormNoteRef, dispatch: props.dispatch, editorRef, - }); - const formNote_beforeLoad = useCallback(async (event: OnLoadEvent) => { - await saveNoteIfWillChange(event.formNote); - setShowRevisions(false); - }, [saveNoteIfWillChange]); - - const formNote_afterLoad = useCallback(async () => { - setTitleHasBeenManuallyChanged(false); - }, []); - - const effectiveNoteId = useEffectiveNoteId(props); - - const { formNote, setFormNote, isNewNote, resourceInfos } = useFormNote({ - syncStarted: props.syncStarted, - decryptionStarted: props.decryptionStarted, - noteId: effectiveNoteId, - isProvisional: props.isProvisional, - titleInputRef: titleInputRef, - editorRef: editorRef, - onBeforeLoad: formNote_beforeLoad, - onAfterLoad: formNote_afterLoad, - }); - setFormNoteRef.current = setFormNote; - const formNoteRef = useRef(); - formNoteRef.current = { ...formNote }; - - const formNoteFolder = useFolder({ folderId: formNote.parent_id }); - - const { - localSearch, - onChange: localSearch_change, - onNext: localSearch_next, - onPrevious: localSearch_previous, - onClose: localSearch_close, - setResultCount: setLocalSearchResultCount, - showLocalSearch, - setShowLocalSearch, - searchMarkers: localSearchMarkerOptions, - } = useNoteSearchBar({ noteSearchBarRef }); - - // If the note has been modified in another editor, wait for it to be saved - // before loading it in this editor. - // const waitingToSaveNote = props.noteId && formNote.id !== props.noteId && props.editorNoteStatuses[props.noteId] === 'saving'; - - const styles = styles_(props); - - const whiteBackgroundNoteRendering = formNote.markup_language === MarkupLanguage.Html; - - const markupToHtml = useMarkupToHtml({ - themeId: props.themeId, - whiteBackgroundNoteRendering, - customCss: props.customCss, - plugins: props.plugins, - settingValue: getPluginSettingValue, - }); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - const allAssets = useCallback(async (markupLanguage: number, options: AllAssetsOptions = null): Promise => { - options = { - contentMaxWidthTarget: '', - ...options, - }; - - const theme = themeStyle(options.themeId ? options.themeId : props.themeId); - - const markupToHtml = markupLanguageUtils.newMarkupToHtml({}, { - resourceBaseUrl: `joplin-content://note-viewer/${Setting.value('resourceDir')}/`, - customCss: props.customCss, - }); - - return markupToHtml.allAssets(markupLanguage, theme, { - contentMaxWidth: props.contentMaxWidth, - contentMaxWidthTarget: options.contentMaxWidthTarget, - whiteBackgroundNoteRendering: options.whiteBackgroundNoteRendering, - }); - }, [props.themeId, props.customCss, props.contentMaxWidth]); - - const handleProvisionalFlag = useCallback(() => { - if (props.isProvisional) { - props.dispatch({ - type: 'NOTE_PROVISIONAL_FLAG_CLEAR', - id: formNote.id, - }); - } - }, [props.isProvisional, formNote.id, props.dispatch]); - - const scheduleNoteListResort = useMemo(() => { - return debounce(() => { - // Although the note list will update automatically, it may take some time. This - // forces an immediate update. - props.dispatch({ type: 'NOTE_SORT' }); - }, 100); - }, [props.dispatch]); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - const onFieldChange = useCallback(async (field: string, value: any, changeId = 0) => { - if (!isMountedRef.current) { - // When the component is unmounted, various actions can happen which can - // trigger onChange events, for example the textarea might be cleared. - // We need to ignore these events, otherwise the note is going to be saved - // with an invalid body. - reg.logger().debug('Skipping change event because the component is unmounted'); - return; - } - - handleProvisionalFlag(); - - const change = field === 'body' ? { - body: value, - } : { - title: value, - }; - - const newNote = { - ...formNote, - ...change, - bodyWillChangeId: 0, - bodyChangeId: 0, - hasChanged: true, - }; - - if (field === 'title') { - setTitleHasBeenManuallyChanged(true); - } - - if (isNewNote && !titleHasBeenManuallyChanged && field === 'body') { - // TODO: Handle HTML/Markdown format - newNote.title = Note.defaultTitle(value); - } - - if (changeId !== null && field === 'body' && formNote.bodyWillChangeId !== changeId) { - // Note was changed, but another note was loaded before save - skipping - // The previously loaded note, that was modified, will be saved via saveNoteIfWillChange() - } else { - setFormNote(newNote); - await scheduleSaveNote(newNote); - } - - if (field === 'title') { - // Scheduling a resort needs to be: - // - called after scheduleSaveNote so that the new note title is used for sorting - // - debounced because many calls to scheduleSaveNote can resolve at once - scheduleNoteListResort(); - } - }, [handleProvisionalFlag, formNote, setFormNote, isNewNote, titleHasBeenManuallyChanged, scheduleNoteListResort, scheduleSaveNote]); - - const onDrop = useDropHandler({ editorRef }); - - const onBodyChange = useCallback((event: OnChangeEvent) => onFieldChange('body', event.content, event.changeId), [onFieldChange]); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - const onTitleChange = useCallback((event: any) => onFieldChange('title', event.target.value), [onFieldChange]); - - const containerRef = useRef(null); - useWindowCommandHandler({ - dispatch: props.dispatch, - setShowLocalSearch, - noteSearchBarRef, - editorRef, - titleInputRef, - onBodyChange, - containerRef, - }); - - // const onTitleKeydown = useCallback((event:any) => { - // const keyCode = event.keyCode; - - // if (keyCode === 9) { - // // TAB - // event.preventDefault(); - - // if (event.shiftKey) { - // CommandService.instance().execute('focusElement', 'noteList'); - // } else { - // CommandService.instance().execute('focusElement', 'noteBody'); - // } - // } - // }, [props.dispatch]); - - const shareCache = useMemo(() => { - return parseShareCache(props.shareCacheSetting); - }, [props.shareCacheSetting]); - - useAsyncEffect(async event => { - if (!formNote.id) return; - - try { - const result = await itemIsReadOnly(BaseItem, ModelType.Note, ItemChange.SOURCE_UNSPECIFIED, formNote.id, props.syncUserId, shareCache); - if (event.cancelled) return; - setIsReadOnly(result); - } catch (error) { - if (error.code === ErrorCode.NotFound) { - // Can happen if the note has been deleted but a render is - // triggered anyway. It can be ignored. - } else { - throw error; - } - } - }, [formNote.id, props.syncUserId, shareCache]); +import { Dispatch } from 'redux'; +import NewWindowOrIFrame from '../NewWindowOrIFrame'; +import WindowCommandHandler from '../WindowCommandHandler/WindowCommandHandler'; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - const onBodyWillChange = useCallback((event: any) => { - handleProvisionalFlag(); +import * as toggleEditors from './commands/toggleEditors'; - setFormNote(prev => { - return { - ...prev, - bodyWillChangeId: event.changeId, - hasChanged: true, - }; - }); - - props.dispatch({ - type: 'EDITOR_NOTE_STATUS_SET', - id: formNote.id, - status: 'saving', - }); - }, [formNote, setFormNote, handleProvisionalFlag, props.dispatch]); - - const { scrollWhenReady, clearScrollWhenReady } = useScrollWhenReadyOptions({ - noteId: formNote.id, - selectedNoteHash: props.selectedNoteHash, - lastEditorScrollPercents: props.lastEditorScrollPercents, - editorRef, - }); - const onMessage = useMessageHandler(scrollWhenReady, clearScrollWhenReady, editorRef, setLocalSearchResultCount, props.dispatch, formNote, htmlToMarkdown, markupToHtml); +const { StyleSheetManager } = require('styled-components'); +// Note: Transitive dependencies used only by react-select. Remove if react-select is removed. +import createCache from '@emotion/cache'; +import { CacheProvider } from '@emotion/react'; +import { stateUtils } from '@joplin/lib/reducer'; +import CommandService from '@joplin/lib/services/CommandService'; +import getWindowCommandPriority from './utils/getWindowCommandPriority'; +import Setting from '@joplin/lib/models/Setting'; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - const externalEditWatcher_noteChange = useCallback((event: any) => { - if (event.id === formNote.id) { - const newFormNote = { - ...formNote, - title: event.note.title, - body: event.note.body, - }; +interface Props { + dispatch: Dispatch; + themeId: number; - setFormNote(newFormNote); - } - }, [formNote, setFormNote]); + isSafeMode: boolean; + codeView: boolean; + legacyMarkdown: boolean; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - const onNotePropertyChange = useCallback((event: any) => { - setFormNote(formNote => { - if (formNote.id !== event.note.id) return formNote; + newWindow: boolean; + windowId: string; + activeWindowId: string; - const newFormNote: FormNote = { ...formNote }; + secondaryWindowNoteIds: Record; + provisionalNoteIds: string[]; +} - for (const key in event.note) { - if (key === 'id') continue; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - (newFormNote as any)[key] = event.note[key]; - } +interface UseToggleEditorsProps { + isSafeMode: boolean; + codeView: boolean; + legacyMarkdown: boolean; + container: RefObject; +} - return newFormNote; - }); - }, [setFormNote]); +const useToggleEditors = (props: UseToggleEditorsProps) => { + const [codeView, setCodeView] = useState(props.codeView); useEffect(() => { - eventManager.on(EventName.AlarmChange, onNotePropertyChange); - ExternalEditWatcher.instance().on('noteChange', externalEditWatcher_noteChange); + const runtime = toggleEditors.runtime(setCodeView); + const registeredRuntime = CommandService.instance().registerRuntime( + toggleEditors.declaration.name, + { ...runtime, getPriority: () => getWindowCommandPriority(props.container) }, + true, + ); return () => { - eventManager.off(EventName.AlarmChange, onNotePropertyChange); - ExternalEditWatcher.instance().off('noteChange', externalEditWatcher_noteChange); + registeredRuntime.deregister(); }; - }, [externalEditWatcher_noteChange, onNotePropertyChange]); + }, [props.container]); useEffect(() => { - const dependencies = { - setShowRevisions, - }; - - CommandService.instance().componentRegisterCommands(dependencies, commands); - - return () => { - CommandService.instance().componentUnregisterCommands(commands); - }; - }, [setShowRevisions]); - - const onScroll = useCallback((event: { percent: number }) => { - props.dispatch({ - type: 'EDITOR_SCROLL_PERCENT_SET', - // In callbacks of setTimeout()/setInterval(), props/state cannot be used - // to refer the current value, since they would be one or more generations old. - // For the purpose, useRef value should be used. - noteId: formNoteRef.current.id, - percent: event.percent, - }); - }, [props.dispatch]); - - function renderNoNotes(rootStyle: React.CSSProperties) { - const emptyDivStyle = { - backgroundColor: 'black', - opacity: 0.1, - ...rootStyle, - }; - return
; - } - - function renderTagButton() { - return ; - } - - function renderTagBar() { - const theme = themeStyle(props.themeId); - const noteIds = [formNote.id]; - const instructions = { void CommandService.instance().execute('setTags', noteIds); }} style={{ ...theme.clickableTextStyle, whiteSpace: 'nowrap' }}>{_('Click to add tags...')}; - const tagList = props.selectedNoteTags.length ? : null; - - return ( -
{tagList}{instructions}
- ); - } - - const searchMarkers = useSearchMarkers(showLocalSearch, localSearchMarkerOptions, props.searches, props.selectedSearchId, props.highlightedWords); - - const editorProps: NoteBodyEditorProps = { - ref: editorRef, - contentKey: formNote.id, - style: styles.tinyMCE, - whiteBackgroundNoteRendering, - onChange: onBodyChange, - onWillChange: onBodyWillChange, - onMessage: onMessage, - content: formNote.body, - contentMarkupLanguage: formNote.markup_language, - contentOriginalCss: formNote.originalCss, - resourceInfos: resourceInfos, - resourceDirectory: Setting.value('resourceDir'), - htmlToMarkdown: htmlToMarkdown, - markupToHtml: markupToHtml, - allAssets: allAssets, - disabled: isReadOnly, - themeId: props.themeId, - dispatch: props.dispatch, - noteToolbar: null, - onScroll: onScroll, - setLocalSearchResultCount: setLocalSearchResultCount, - setLocalSearch: localSearch_change, - setShowLocalSearch, - useLocalSearch: showLocalSearch, - searchMarkers: searchMarkers, - visiblePanes: props.noteVisiblePanes || ['editor', 'viewer'], - keyboardMode: Setting.value('editor.keyboardMode'), - locale: Setting.value('locale'), - onDrop: onDrop, - noteToolbarButtonInfos: props.toolbarButtonInfos, - plugins: props.plugins, - fontSize: Setting.value('style.editor.fontSize'), - contentMaxWidth: props.contentMaxWidth, - isSafeMode: props.isSafeMode, - useCustomPdfViewer: props.useCustomPdfViewer, - // We need it to identify the context for which media is rendered. - // It is currently used to remember pdf scroll position for each attachments of each note uniquely. - noteId: props.noteId, - watchedNoteFiles: props.watchedNoteFiles, - }; - - let editor = null; + Setting.setValue('editor.codeView', codeView); + }, [codeView]); - if (props.bodyEditor === 'TinyMCE') { - editor = ; - } else if (props.bodyEditor === 'PlainText') { - editor = ; - } else if (props.bodyEditor === 'CodeMirror5') { - editor = ; - } else if (props.bodyEditor === 'CodeMirror6') { - editor = ; - } else { - throw new Error(`Invalid editor: ${props.bodyEditor}`); - } + return useMemo(() => { + let bodyEditor = codeView ? 'CodeMirror6' : 'TinyMCE'; - const noteRevisionViewer_onBack = useCallback(() => { - setShowRevisions(false); - }, []); - - const onBannerResourceClick = useCallback(async (event: React.MouseEvent) => { - event.preventDefault(); - const resourceId = event.currentTarget.getAttribute('data-resource-id'); - await openItemById(resourceId, props.dispatch); - }, [props.dispatch]); - - if (showRevisions) { - const theme = themeStyle(props.themeId); - - const revStyle: React.CSSProperties = { - // ...props.style, - display: 'inline-flex', - padding: theme.margin, - verticalAlign: 'top', - boxSizing: 'border-box', - }; - - return ( -
- -
- ); - } - - if (props.selectedNoteIds.length > 1) { - return ; - } - - function renderSearchBar() { - if (!showLocalSearch) return false; - - const theme = themeStyle(props.themeId); - - return ( - - ); - } - - function renderResourceWatchingNotification() { - if (!Object.keys(props.watchedResources).length) return null; - const resourceTitles = Object.keys(props.watchedResources).map(id => props.watchedResources[id].title); - return ( -
-

{_('The following attachments are being watched for changes:')} {resourceTitles.join(', ')}

-

{_('The attachments will no longer be watched when you switch to a different note.')}

-
- ); - } - - const renderResourceInSearchResultsNotification = () => { - const resourceResults = props.searchResults.filter(r => r.id === props.noteId && r.item_type === ModelType.Resource); - if (!resourceResults.length) return null; + if (props.isSafeMode) { + bodyEditor = 'PlainText'; + } else if (codeView && props.legacyMarkdown) { + bodyEditor = 'CodeMirror5'; + } - const renderResource = (id: string, title: string) => { - return
  • {title}
  • ; - }; + return bodyEditor; + }, [codeView, props.isSafeMode, props.legacyMarkdown]); +}; - return ( -
    -

    {_n('The following attachment matches your search query:', 'The following attachments match your search query:', resourceResults.length)}

    -
      - {resourceResults.map(r => renderResource(r.item_id, r.title))} -
    -
    - ); - }; +const NoteEditorWrapper: React.FC = props => { + const containerRef = useRef(null); - function renderSearchInfo() { - const theme = themeStyle(props.themeId); - if (formNoteFolder && ['Search', 'Tag', 'SmartFilter'].includes(props.notesParentType)) { - return ( -
    -
    - ); - } else { - return null; + const bodyEditor = useToggleEditors({ + isSafeMode: props.isSafeMode, codeView: props.codeView, legacyMarkdown: props.legacyMarkdown, container: containerRef, + }); + const noteId = props.secondaryWindowNoteIds[props.windowId][0] ?? ''; + const editor = noteId ?
    + +
    : null; + + const newWindow = props.newWindow; + const onWindowClose = useCallback(() => { + if (newWindow) { + props.dispatch({ type: 'WINDOW_CLOSE', windowId: props.windowId }); } - } - - if (formNote.encryption_applied || !formNote.id || !effectiveNoteId) { - return renderNoNotes(styles.root); - } - - const theme = themeStyle(props.themeId); + }, [props.dispatch, props.windowId, newWindow]); + + const onWindowFocus = useCallback(() => { + props.dispatch({ type: 'WINDOW_FOCUS', windowId: props.windowId }); + }, [props.dispatch, props.windowId]); + + const windowContent = + + + {editor} + + + ; + return newWindow ? windowContent : editor; +}; - return ( -
    -
    - {renderResourceWatchingNotification()} - {renderResourceInSearchResultsNotification()} - - {renderSearchInfo()} -
    - {editor} -
    -
    - {renderSearchBar()} -
    -
    - {renderTagButton()} - {renderTagBar()} -
    - -
    -
    - ); +interface StyleProviderProps { + children: React.ReactNode[]|React.ReactNode; } -export { - NoteEditor as NoteEditorComponent, +// Sets the root style container for libraries. At present, this is needed by react-select (which uses @emotion/...) +// and styled-components. +// See: https://github.com/JedWatson/react-select/issues/3680 and https://github.com/styled-components/styled-components/issues/659 +const LibraryStyleRoot: React.FC = props => { + const [dependencyStyleContainer, setDependencyStyleContainer] = useState(null); + const cache = useMemo(() => { + return createCache({ + key: 'new-window-cache', + container: dependencyStyleContainer, + }); + }, [dependencyStyleContainer]); + + return <> +
    + + + {props.children} + + + ; }; -const mapStateToProps = (state: AppState) => { - const whenClauseContext = stateToWhenClauseContext(state); - +export default connect((state: AppState) => { return { - notes: state.notes, - selectedNoteIds: state.selectedNoteIds, - selectedFolderId: state.selectedFolderId, - editorNoteStatuses: state.editorNoteStatuses, - syncStarted: state.syncStarted, - decryptionStarted: state.decryptionWorker?.state !== 'idle', themeId: state.settings.theme, - watchedNoteFiles: state.watchedNoteFiles, - notesParentType: state.notesParentType, - selectedNoteTags: state.selectedNoteTags, - lastEditorScrollPercents: state.lastEditorScrollPercents, - selectedNoteHash: state.selectedNoteHash, - searches: state.searches, - selectedSearchId: state.selectedSearchId, - customCss: state.customCss, - noteVisiblePanes: state.noteVisiblePanes, - watchedResources: state.watchedResources, - highlightedWords: state.highlightedWords, - plugins: state.pluginService.plugins, - toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons([ - 'historyBackward', - 'historyForward', - 'toggleEditors', - 'toggleExternalEditing', - ], whenClauseContext), - setTagsToolbarButtonInfo: toolbarButtonUtils.commandsToToolbarButtons([ - 'setTags', - ], whenClauseContext)[0], - contentMaxWidth: state.settings['style.editor.contentMaxWidth'], isSafeMode: state.settings.isSafeMode, - useCustomPdfViewer: false, - syncUserId: state.settings['sync.userId'], - shareCacheSetting: state.settings['sync.shareCache'], - searchResults: state.searchResults, + codeView: state.settings['editor.codeView'], + legacyMarkdown: state.settings['editor.legacyMarkdown'], + provisionalNoteIds: state.provisionalNoteIds, + secondaryWindowNoteIds: stateUtils.windowIdToSelectedNoteIds(state), + activeWindowId: stateUtils.activeWindowId(state), }; -}; - -export default connect(mapStateToProps)(NoteEditor); +})(NoteEditorWrapper); diff --git a/packages/app-desktop/gui/NoteEditor/NoteEditorContent.tsx b/packages/app-desktop/gui/NoteEditor/NoteEditorContent.tsx new file mode 100644 index 00000000000..132d0324151 --- /dev/null +++ b/packages/app-desktop/gui/NoteEditor/NoteEditorContent.tsx @@ -0,0 +1,656 @@ +import * as React from 'react'; +import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; +import TinyMCE from './NoteBody/TinyMCE/TinyMCE'; +import { connect } from 'react-redux'; +import MultiNoteActions from '../MultiNoteActions'; +import { htmlToMarkdown } from './utils'; +import useSearchMarkers from './utils/useSearchMarkers'; +import useNoteSearchBar from './utils/useNoteSearchBar'; +import useMessageHandler from './utils/useMessageHandler'; +import useWindowCommandHandler from './utils/useWindowCommandHandler'; +import useDropHandler from './utils/useDropHandler'; +import useMarkupToHtml from './utils/useMarkupToHtml'; +import useFormNote, { OnLoadEvent, OnSetFormNote } from './utils/useFormNote'; +import useEffectiveNoteId from './utils/useEffectiveNoteId'; +import useFolder from './utils/useFolder'; +import styles_ from './styles'; +import { NoteEditorProps, FormNote, OnChangeEvent, NoteBodyEditorProps, AllAssetsOptions, NoteBodyEditorRef } from './utils/types'; +import CommandService from '@joplin/lib/services/CommandService'; +import ToolbarButton from '../ToolbarButton/ToolbarButton'; +import Button, { ButtonLevel } from '../Button/Button'; +import eventManager, { EventName } from '@joplin/lib/eventManager'; +import { AppState } from '../../app.reducer'; +import ToolbarButtonUtils from '@joplin/lib/services/commands/ToolbarButtonUtils'; +import { _, _n } from '@joplin/lib/locale'; +import TagList from '../TagList'; +import NoteTitleBar from './NoteTitle/NoteTitleBar'; +import markupLanguageUtils from '../../utils/markupLanguageUtils'; +import Setting from '@joplin/lib/models/Setting'; +import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext'; +import ExternalEditWatcher from '@joplin/lib/services/ExternalEditWatcher'; +import { itemIsReadOnly } from '@joplin/lib/models/utils/readOnly'; +const { themeStyle } = require('@joplin/lib/theme'); +const { substrWithEllipsis } = require('@joplin/lib/string-utils'); +import NoteSearchBar from '../NoteSearchBar'; +import { reg } from '@joplin/lib/registry'; +import Note from '@joplin/lib/models/Note'; +import Folder from '@joplin/lib/models/Folder'; +import NoteRevisionViewer from '../NoteRevisionViewer'; +import { parseShareCache } from '@joplin/lib/services/share/reducer'; +import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect'; +import { ModelType } from '@joplin/lib/BaseModel'; +import BaseItem from '@joplin/lib/models/BaseItem'; +import { ErrorCode } from '@joplin/lib/errors'; +import ItemChange from '@joplin/lib/models/ItemChange'; +import PlainEditor from './NoteBody/PlainEditor/PlainEditor'; +import CodeMirror6 from './NoteBody/CodeMirror/v6/CodeMirror'; +import CodeMirror5 from './NoteBody/CodeMirror/v5/CodeMirror'; +import { openItemById } from './utils/contextMenu'; +import getPluginSettingValue from '@joplin/lib/services/plugins/utils/getPluginSettingValue'; +import { MarkupLanguage } from '@joplin/renderer'; +import useScrollWhenReadyOptions from './utils/useScrollWhenReadyOptions'; +import useScheduleSaveCallbacks from './utils/useScheduleSaveCallbacks'; +import WarningBanner from './WarningBanner/WarningBanner'; +const debounce = require('debounce'); + +const commands = [ + require('./commands/showRevisions'), +]; + +const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance()); + +function NoteEditorContent(props: NoteEditorProps) { + const [showRevisions, setShowRevisions] = useState(false); + const [titleHasBeenManuallyChanged, setTitleHasBeenManuallyChanged] = useState(false); + const [isReadOnly, setIsReadOnly] = useState(false); + + const editorRef = useRef(); + const titleInputRef = useRef(); + const isMountedRef = useRef(true); + const noteSearchBarRef = useRef(null); + + const setFormNoteRef = useRef(); + const { saveNoteIfWillChange, scheduleSaveNote } = useScheduleSaveCallbacks({ + setFormNote: setFormNoteRef, dispatch: props.dispatch, editorRef, + }); + const formNote_beforeLoad = useCallback(async (event: OnLoadEvent) => { + await saveNoteIfWillChange(event.formNote); + setShowRevisions(false); + }, [saveNoteIfWillChange]); + + const formNote_afterLoad = useCallback(async () => { + setTitleHasBeenManuallyChanged(false); + }, []); + + const effectiveNoteId = useEffectiveNoteId(props); + + const { formNote, setFormNote, isNewNote, resourceInfos } = useFormNote({ + syncStarted: props.syncStarted, + decryptionStarted: props.decryptionStarted, + noteId: effectiveNoteId, + isProvisional: props.isProvisional, + titleInputRef: titleInputRef, + editorRef: editorRef, + onBeforeLoad: formNote_beforeLoad, + onAfterLoad: formNote_afterLoad, + }); + setFormNoteRef.current = setFormNote; + const formNoteRef = useRef(); + formNoteRef.current = { ...formNote }; + + const formNoteFolder = useFolder({ folderId: formNote.parent_id }); + + const { + localSearch, + onChange: localSearch_change, + onNext: localSearch_next, + onPrevious: localSearch_previous, + onClose: localSearch_close, + setResultCount: setLocalSearchResultCount, + showLocalSearch, + setShowLocalSearch, + searchMarkers: localSearchMarkerOptions, + } = useNoteSearchBar({ noteSearchBarRef }); + + // If the note has been modified in another editor, wait for it to be saved + // before loading it in this editor. + // const waitingToSaveNote = props.noteId && formNote.id !== props.noteId && props.editorNoteStatuses[props.noteId] === 'saving'; + + const styles = styles_(props); + + const whiteBackgroundNoteRendering = formNote.markup_language === MarkupLanguage.Html; + + const markupToHtml = useMarkupToHtml({ + themeId: props.themeId, + whiteBackgroundNoteRendering, + customCss: props.customCss, + plugins: props.plugins, + settingValue: getPluginSettingValue, + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + const allAssets = useCallback(async (markupLanguage: number, options: AllAssetsOptions = null): Promise => { + options = { + contentMaxWidthTarget: '', + ...options, + }; + + const theme = themeStyle(options.themeId ? options.themeId : props.themeId); + + const markupToHtml = markupLanguageUtils.newMarkupToHtml({}, { + resourceBaseUrl: `joplin-content://note-viewer/${Setting.value('resourceDir')}/`, + customCss: props.customCss, + }); + + return markupToHtml.allAssets(markupLanguage, theme, { + contentMaxWidth: props.contentMaxWidth, + contentMaxWidthTarget: options.contentMaxWidthTarget, + whiteBackgroundNoteRendering: options.whiteBackgroundNoteRendering, + }); + }, [props.themeId, props.customCss, props.contentMaxWidth]); + + const handleProvisionalFlag = useCallback(() => { + if (props.isProvisional) { + props.dispatch({ + type: 'NOTE_PROVISIONAL_FLAG_CLEAR', + id: formNote.id, + }); + } + }, [props.isProvisional, formNote.id, props.dispatch]); + + const scheduleNoteListResort = useMemo(() => { + return debounce(() => { + // Although the note list will update automatically, it may take some time. This + // forces an immediate update. + props.dispatch({ type: 'NOTE_SORT' }); + }, 100); + }, [props.dispatch]); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + const onFieldChange = useCallback(async (field: string, value: any, changeId = 0) => { + if (!isMountedRef.current) { + // When the component is unmounted, various actions can happen which can + // trigger onChange events, for example the textarea might be cleared. + // We need to ignore these events, otherwise the note is going to be saved + // with an invalid body. + reg.logger().debug('Skipping change event because the component is unmounted'); + return; + } + + handleProvisionalFlag(); + + const change = field === 'body' ? { + body: value, + } : { + title: value, + }; + + const newNote = { + ...formNote, + ...change, + bodyWillChangeId: 0, + bodyChangeId: 0, + hasChanged: true, + }; + + if (field === 'title') { + setTitleHasBeenManuallyChanged(true); + } + + if (isNewNote && !titleHasBeenManuallyChanged && field === 'body') { + // TODO: Handle HTML/Markdown format + newNote.title = Note.defaultTitle(value); + } + + if (changeId !== null && field === 'body' && formNote.bodyWillChangeId !== changeId) { + // Note was changed, but another note was loaded before save - skipping + // The previously loaded note, that was modified, will be saved via saveNoteIfWillChange() + } else { + setFormNote(newNote); + await scheduleSaveNote(newNote); + } + + if (field === 'title') { + // Scheduling a resort needs to be: + // - called after scheduleSaveNote so that the new note title is used for sorting + // - debounced because many calls to scheduleSaveNote can resolve at once + scheduleNoteListResort(); + } + }, [handleProvisionalFlag, formNote, setFormNote, isNewNote, titleHasBeenManuallyChanged, scheduleNoteListResort, scheduleSaveNote]); + + const onDrop = useDropHandler({ editorRef }); + + const onBodyChange = useCallback((event: OnChangeEvent) => onFieldChange('body', event.content, event.changeId), [onFieldChange]); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + const onTitleChange = useCallback((event: any) => onFieldChange('title', event.target.value), [onFieldChange]); + + const containerRef = useRef(null); + useWindowCommandHandler({ + dispatch: props.dispatch, + setShowLocalSearch, + noteSearchBarRef, + editorRef, + titleInputRef, + onBodyChange, + containerRef, + }); + + // const onTitleKeydown = useCallback((event:any) => { + // const keyCode = event.keyCode; + + // if (keyCode === 9) { + // // TAB + // event.preventDefault(); + + // if (event.shiftKey) { + // CommandService.instance().execute('focusElement', 'noteList'); + // } else { + // CommandService.instance().execute('focusElement', 'noteBody'); + // } + // } + // }, [props.dispatch]); + + const shareCache = useMemo(() => { + return parseShareCache(props.shareCacheSetting); + }, [props.shareCacheSetting]); + + useAsyncEffect(async event => { + if (!formNote.id) return; + + try { + const result = await itemIsReadOnly(BaseItem, ModelType.Note, ItemChange.SOURCE_UNSPECIFIED, formNote.id, props.syncUserId, shareCache); + if (event.cancelled) return; + setIsReadOnly(result); + } catch (error) { + if (error.code === ErrorCode.NotFound) { + // Can happen if the note has been deleted but a render is + // triggered anyway. It can be ignored. + } else { + throw error; + } + } + }, [formNote.id, props.syncUserId, shareCache]); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + const onBodyWillChange = useCallback((event: any) => { + handleProvisionalFlag(); + + setFormNote(prev => { + return { + ...prev, + bodyWillChangeId: event.changeId, + hasChanged: true, + }; + }); + + props.dispatch({ + type: 'EDITOR_NOTE_STATUS_SET', + id: formNote.id, + status: 'saving', + }); + }, [formNote, setFormNote, handleProvisionalFlag, props.dispatch]); + + const { scrollWhenReady, clearScrollWhenReady } = useScrollWhenReadyOptions({ + noteId: formNote.id, + selectedNoteHash: props.selectedNoteHash, + lastEditorScrollPercents: props.lastEditorScrollPercents, + editorRef, + }); + const onMessage = useMessageHandler(scrollWhenReady, clearScrollWhenReady, editorRef, setLocalSearchResultCount, props.dispatch, formNote, htmlToMarkdown, markupToHtml); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + const externalEditWatcher_noteChange = useCallback((event: any) => { + if (event.id === formNote.id) { + const newFormNote = { + ...formNote, + title: event.note.title, + body: event.note.body, + }; + + setFormNote(newFormNote); + } + }, [formNote, setFormNote]); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + const onNotePropertyChange = useCallback((event: any) => { + setFormNote(formNote => { + if (formNote.id !== event.note.id) return formNote; + + const newFormNote: FormNote = { ...formNote }; + + for (const key in event.note) { + if (key === 'id') continue; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + (newFormNote as any)[key] = event.note[key]; + } + + return newFormNote; + }); + }, [setFormNote]); + + useEffect(() => { + eventManager.on(EventName.AlarmChange, onNotePropertyChange); + ExternalEditWatcher.instance().on('noteChange', externalEditWatcher_noteChange); + + return () => { + eventManager.off(EventName.AlarmChange, onNotePropertyChange); + ExternalEditWatcher.instance().off('noteChange', externalEditWatcher_noteChange); + }; + }, [externalEditWatcher_noteChange, onNotePropertyChange]); + + useEffect(() => { + const dependencies = { + setShowRevisions, + }; + + CommandService.instance().componentRegisterCommands(dependencies, commands); + + return () => { + CommandService.instance().componentUnregisterCommands(commands); + }; + }, [setShowRevisions]); + + const onScroll = useCallback((event: { percent: number }) => { + props.dispatch({ + type: 'EDITOR_SCROLL_PERCENT_SET', + // In callbacks of setTimeout()/setInterval(), props/state cannot be used + // to refer the current value, since they would be one or more generations old. + // For the purpose, useRef value should be used. + noteId: formNoteRef.current.id, + percent: event.percent, + }); + }, [props.dispatch]); + + function renderNoNotes(rootStyle: React.CSSProperties) { + const emptyDivStyle = { + backgroundColor: 'black', + opacity: 0.1, + ...rootStyle, + }; + return
    ; + } + + function renderTagButton() { + return ; + } + + function renderTagBar() { + const theme = themeStyle(props.themeId); + const noteIds = [formNote.id]; + const instructions = { void CommandService.instance().execute('setTags', noteIds); }} style={{ ...theme.clickableTextStyle, whiteSpace: 'nowrap' }}>{_('Click to add tags...')}; + const tagList = props.selectedNoteTags.length ? : null; + + return ( +
    {tagList}{instructions}
    + ); + } + + const searchMarkers = useSearchMarkers(showLocalSearch, localSearchMarkerOptions, props.searches, props.selectedSearchId, props.highlightedWords); + + const editorProps: NoteBodyEditorProps = { + ref: editorRef, + contentKey: formNote.id, + style: styles.tinyMCE, + whiteBackgroundNoteRendering, + onChange: onBodyChange, + onWillChange: onBodyWillChange, + onMessage: onMessage, + content: formNote.body, + contentMarkupLanguage: formNote.markup_language, + contentOriginalCss: formNote.originalCss, + resourceInfos: resourceInfos, + resourceDirectory: Setting.value('resourceDir'), + htmlToMarkdown: htmlToMarkdown, + markupToHtml: markupToHtml, + allAssets: allAssets, + disabled: isReadOnly, + themeId: props.themeId, + dispatch: props.dispatch, + noteToolbar: null, + onScroll: onScroll, + setLocalSearchResultCount: setLocalSearchResultCount, + setLocalSearch: localSearch_change, + setShowLocalSearch, + useLocalSearch: showLocalSearch, + searchMarkers: searchMarkers, + visiblePanes: props.noteVisiblePanes || ['editor', 'viewer'], + keyboardMode: Setting.value('editor.keyboardMode'), + locale: Setting.value('locale'), + onDrop: onDrop, + noteToolbarButtonInfos: props.toolbarButtonInfos, + plugins: props.plugins, + fontSize: Setting.value('style.editor.fontSize'), + contentMaxWidth: props.contentMaxWidth, + isSafeMode: props.isSafeMode, + useCustomPdfViewer: props.useCustomPdfViewer, + // We need it to identify the context for which media is rendered. + // It is currently used to remember pdf scroll position for each attachments of each note uniquely. + noteId: props.noteId, + watchedNoteFiles: props.watchedNoteFiles, + }; + + let editor = null; + + if (props.bodyEditor === 'TinyMCE') { + editor = ; + } else if (props.bodyEditor === 'PlainText') { + editor = ; + } else if (props.bodyEditor === 'CodeMirror5') { + editor = ; + } else if (props.bodyEditor === 'CodeMirror6') { + editor = ; + } else { + throw new Error(`Invalid editor: ${props.bodyEditor}`); + } + + const noteRevisionViewer_onBack = useCallback(() => { + setShowRevisions(false); + }, []); + + const onBannerResourceClick = useCallback(async (event: React.MouseEvent) => { + event.preventDefault(); + const resourceId = event.currentTarget.getAttribute('data-resource-id'); + await openItemById(resourceId, props.dispatch); + }, [props.dispatch]); + + if (showRevisions) { + const theme = themeStyle(props.themeId); + + const revStyle: React.CSSProperties = { + // ...props.style, + display: 'inline-flex', + padding: theme.margin, + verticalAlign: 'top', + boxSizing: 'border-box', + }; + + return ( +
    + +
    + ); + } + + if (props.selectedNoteIds.length > 1) { + return ; + } + + function renderSearchBar() { + if (!showLocalSearch) return false; + + const theme = themeStyle(props.themeId); + + return ( + + ); + } + + function renderResourceWatchingNotification() { + if (!Object.keys(props.watchedResources).length) return null; + const resourceTitles = Object.keys(props.watchedResources).map(id => props.watchedResources[id].title); + return ( +
    +

    {_('The following attachments are being watched for changes:')} {resourceTitles.join(', ')}

    +

    {_('The attachments will no longer be watched when you switch to a different note.')}

    +
    + ); + } + + const renderResourceInSearchResultsNotification = () => { + const resourceResults = props.searchResults.filter(r => r.id === props.noteId && r.item_type === ModelType.Resource); + if (!resourceResults.length) return null; + + const renderResource = (id: string, title: string) => { + return
  • {title}
  • ; + }; + + return ( +
    +

    {_n('The following attachment matches your search query:', 'The following attachments match your search query:', resourceResults.length)}

    +
      + {resourceResults.map(r => renderResource(r.item_id, r.title))} +
    +
    + ); + }; + + function renderSearchInfo() { + const theme = themeStyle(props.themeId); + if (formNoteFolder && ['Search', 'Tag', 'SmartFilter'].includes(props.notesParentType)) { + return ( +
    +
    + ); + } else { + return null; + } + } + + if (formNote.encryption_applied || !formNote.id || !effectiveNoteId) { + return renderNoNotes(styles.root); + } + + const theme = themeStyle(props.themeId); + + return ( +
    +
    + {renderResourceWatchingNotification()} + {renderResourceInSearchResultsNotification()} + + {renderSearchInfo()} +
    + {editor} +
    +
    + {renderSearchBar()} +
    +
    + {renderTagButton()} + {renderTagBar()} +
    + +
    +
    + ); +} + +export { + NoteEditorContent as NoteEditorComponent, +}; + +const mapStateToProps = (state: AppState) => { + const whenClauseContext = stateToWhenClauseContext(state); + + return { + notes: state.notes, + selectedNoteIds: state.selectedNoteIds, + selectedFolderId: state.selectedFolderId, + editorNoteStatuses: state.editorNoteStatuses, + syncStarted: state.syncStarted, + decryptionStarted: state.decryptionWorker?.state !== 'idle', + themeId: state.settings.theme, + watchedNoteFiles: state.watchedNoteFiles, + notesParentType: state.notesParentType, + selectedNoteTags: state.selectedNoteTags, + lastEditorScrollPercents: state.lastEditorScrollPercents, + selectedNoteHash: state.selectedNoteHash, + searches: state.searches, + selectedSearchId: state.selectedSearchId, + customCss: state.customCss, + noteVisiblePanes: state.noteVisiblePanes, + watchedResources: state.watchedResources, + highlightedWords: state.highlightedWords, + plugins: state.pluginService.plugins, + toolbarButtonInfos: toolbarButtonUtils.commandsToToolbarButtons([ + 'historyBackward', + 'historyForward', + 'toggleEditors', + 'toggleExternalEditing', + ], whenClauseContext), + setTagsToolbarButtonInfo: toolbarButtonUtils.commandsToToolbarButtons([ + 'setTags', + ], whenClauseContext)[0], + contentMaxWidth: state.settings['style.editor.contentMaxWidth'], + isSafeMode: state.settings.isSafeMode, + useCustomPdfViewer: false, + syncUserId: state.settings['sync.userId'], + shareCacheSetting: state.settings['sync.shareCache'], + searchResults: state.searchResults, + }; +}; + +export default connect(mapStateToProps)(NoteEditorContent); diff --git a/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx b/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx deleted file mode 100644 index 065304a77d7..00000000000 --- a/packages/app-desktop/gui/NoteEditor/NoteEditorWrapper.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import * as React from 'react'; -import { useCallback, useMemo, useState } from 'react'; -import NoteEditor from './NoteEditor'; -import StyleSheetContainer from '../StyleSheets/StyleSheetContainer'; -import { connect } from 'react-redux'; -import { AppState } from '../../app.reducer'; -import { Dispatch } from 'redux'; -import NewWindowOrIFrame from '../NewWindowOrIFrame'; -import WindowCommandHandler from '../WindowCommandHandler/WindowCommandHandler'; - -const { StyleSheetManager } = require('styled-components'); -// Note: Transitive dependencies used only by react-select. Remove if react-select is removed. -import createCache from '@emotion/cache'; -import { CacheProvider } from '@emotion/react'; -import { stateUtils } from '@joplin/lib/reducer'; - -interface Props { - dispatch: Dispatch; - themeId: number; - bodyEditor: string; - newWindow: boolean; - windowId: string; - activeWindowId: string; - - secondaryWindowNoteIds: Record; - provisionalNoteIds: string[]; -} - -const NoteEditorWrapper: React.FC = props => { - const newWindow = props.newWindow; - const onWindowClose = useCallback(() => { - if (newWindow) { - props.dispatch({ type: 'WINDOW_CLOSE', windowId: props.windowId }); - } - }, [props.dispatch, props.windowId, newWindow]); - - const onWindowFocus = useCallback(() => { - props.dispatch({ type: 'WINDOW_FOCUS', windowId: props.windowId }); - }, [props.dispatch, props.windowId]); - - const noteId = props.secondaryWindowNoteIds[props.windowId][0] ?? ''; - const editor = noteId ? : null; - - const windowContent = - - - {editor} - - - ; - return newWindow ? windowContent : editor; -}; - -interface StyleProviderProps { - children: React.ReactNode[]|React.ReactNode; -} - -// Sets the root style container for libraries. At present, this is needed by react-select (which uses @emotion/...) -// and styled-components. -// See: https://github.com/JedWatson/react-select/issues/3680 and https://github.com/styled-components/styled-components/issues/659 -const LibraryStyleRoot: React.FC = props => { - const [dependencyStyleContainer, setDependencyStyleContainer] = useState(null); - const cache = useMemo(() => { - return createCache({ - key: 'new-window-cache', - container: dependencyStyleContainer, - }); - }, [dependencyStyleContainer]); - - return <> -
    - - - {props.children} - - - ; -}; - -export default connect((state: AppState) => { - let bodyEditor = state.settings['editor.codeView'] ? 'CodeMirror6' : 'TinyMCE'; - - if (state.settings.isSafeMode) { - bodyEditor = 'PlainText'; - } else if (state.settings['editor.codeView'] && state.settings['editor.legacyMarkdown']) { - bodyEditor = 'CodeMirror5'; - } - - return { - themeId: state.settings.theme, - provisionalNoteIds: state.provisionalNoteIds, - secondaryWindowNoteIds: stateUtils.windowIdToSelectedNoteIds(state), - activeWindowId: stateUtils.activeWindowId(state), - bodyEditor, - }; -})(NoteEditorWrapper); diff --git a/packages/app-desktop/gui/NoteEditor/commands/index.ts b/packages/app-desktop/gui/NoteEditor/commands/index.ts index 5e4c02028ec..cab822f8ea1 100644 --- a/packages/app-desktop/gui/NoteEditor/commands/index.ts +++ b/packages/app-desktop/gui/NoteEditor/commands/index.ts @@ -4,6 +4,7 @@ import * as focusElementNoteTitle from './focusElementNoteTitle'; import * as pasteAsText from './pasteAsText'; import * as showLocalSearch from './showLocalSearch'; import * as showRevisions from './showRevisions'; +import * as toggleEditors from './toggleEditors'; const index: any[] = [ focusElementNoteBody, @@ -11,6 +12,7 @@ const index: any[] = [ pasteAsText, showLocalSearch, showRevisions, + toggleEditors, ]; export default index; diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/toggleEditors.ts b/packages/app-desktop/gui/NoteEditor/commands/toggleEditors.ts similarity index 77% rename from packages/app-desktop/gui/WindowCommandHandler/commands/toggleEditors.ts rename to packages/app-desktop/gui/NoteEditor/commands/toggleEditors.ts index 60437dc9767..b074ac68b36 100644 --- a/packages/app-desktop/gui/WindowCommandHandler/commands/toggleEditors.ts +++ b/packages/app-desktop/gui/NoteEditor/commands/toggleEditors.ts @@ -1,5 +1,5 @@ +import * as React from 'react'; import { CommandDeclaration, CommandRuntime, CommandContext } from '@joplin/lib/services/CommandService'; -import Setting from '@joplin/lib/models/Setting'; import { stateUtils } from '@joplin/lib/reducer'; import { _ } from '@joplin/lib/locale'; @@ -9,7 +9,9 @@ export const declaration: CommandDeclaration = { iconName: 'fa-columns', }; -export const runtime = (): CommandRuntime => { +type SetCodeViewCallback = React.Dispatch>; + +export const runtime = (setCodeView: SetCodeViewCallback): CommandRuntime => { return { execute: async (context: CommandContext) => { // A bit of a hack, but for now don't allow changing code view @@ -17,7 +19,7 @@ export const runtime = (): CommandRuntime => { // TinyMCE because it won't have time to send its content before // being switch to Ace Editor. if (stateUtils.hasNotesBeingSaved(context.state)) return; - Setting.toggle('editor.codeView'); + setCodeView(codeView => !codeView); }, enabledCondition: '!notesAreBeingSaved && oneNoteSelected', }; diff --git a/packages/app-desktop/gui/NoteEditor/style.scss b/packages/app-desktop/gui/NoteEditor/style.scss index 6fe97444de1..b5436a4df6f 100644 --- a/packages/app-desktop/gui/NoteEditor/style.scss +++ b/packages/app-desktop/gui/NoteEditor/style.scss @@ -3,3 +3,4 @@ @use "./styles/warning-banner-link.scss"; @use "./styles/note-title-info-group.scss"; @use "./styles/note-title-wrapper.scss"; +@use "./styles/note-editor-wrapper.scss"; diff --git a/packages/app-desktop/gui/NoteEditor/styles/note-editor-wrapper.scss b/packages/app-desktop/gui/NoteEditor/styles/note-editor-wrapper.scss new file mode 100644 index 00000000000..9359a1c6f74 --- /dev/null +++ b/packages/app-desktop/gui/NoteEditor/styles/note-editor-wrapper.scss @@ -0,0 +1,8 @@ + +.note-editor-wrapper { + display: flex; + flex-grow: 1; + flex-shrink: 1; + width: 100%; + height: 100%; +} \ No newline at end of file diff --git a/packages/app-desktop/gui/NoteEditor/utils/getWindowCommandPriority.ts b/packages/app-desktop/gui/NoteEditor/utils/getWindowCommandPriority.ts new file mode 100644 index 00000000000..08b759a9f3d --- /dev/null +++ b/packages/app-desktop/gui/NoteEditor/utils/getWindowCommandPriority.ts @@ -0,0 +1,15 @@ +import { RefObject } from 'react'; + +const getWindowCommandPriority = (contentContainer: RefObject) => { + if (!contentContainer.current) return 0; + const containerDocument = contentContainer.current.getRootNode() as Document; + if (!containerDocument || !containerDocument.hasFocus()) return 0; + + if (contentContainer.current.contains(containerDocument.activeElement)) { + return 2; + } + + // Container document has focus, but not this editor. + return 1; +}; +export default getWindowCommandPriority; diff --git a/packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.ts b/packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.ts index e6361500284..595d51e0c4e 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.ts @@ -4,6 +4,7 @@ import editorCommandDeclarations, { enabledCondition } from '../editorCommandDec import CommandService, { CommandDeclaration, CommandRuntime, CommandContext, RegisteredRuntime } from '@joplin/lib/services/CommandService'; import time from '@joplin/lib/time'; import { reg } from '@joplin/lib/registry'; +import getWindowCommandPriority from './getWindowCommandPriority'; const commandsWithDependencies = [ require('../commands/showLocalSearch'), @@ -80,18 +81,7 @@ export default function useWindowCommandHandler(dependencies: HookDependencies) const { setShowLocalSearch, noteSearchBarRef, editorRef, titleInputRef, onBodyChange, containerRef } = dependencies; useEffect(() => { - const getRuntimePriority = () => { - if (!containerRef.current) return 0; - const containerDocument = containerRef.current.getRootNode() as Document; - if (!containerDocument || !containerDocument.hasFocus()) return 0; - - if (containerRef.current.contains(containerDocument.activeElement)) { - return 2; - } - - // Container document has focus, but not this editor. - return 1; - }; + const getRuntimePriority = () => getWindowCommandPriority(containerRef); const deregisterCallbacks: RegisteredRuntime[] = []; for (const declaration of editorCommandDeclarations) { diff --git a/packages/app-desktop/gui/Root.tsx b/packages/app-desktop/gui/Root.tsx index 26bbad211a9..a9b59031953 100644 --- a/packages/app-desktop/gui/Root.tsx +++ b/packages/app-desktop/gui/Root.tsx @@ -30,7 +30,7 @@ import Navigator from './Navigator'; import WelcomeUtils from '@joplin/lib/WelcomeUtils'; import JoplinCloudLoginScreen from './JoplinCloudLoginScreen'; import WindowCommandHandler from './WindowCommandHandler/WindowCommandHandler'; -import NoteEditorWrapper from './NoteEditor/NoteEditorWrapper'; +import NoteEditorWrapper from './NoteEditor/NoteEditor'; import { stateUtils, WindowState } from '@joplin/lib/reducer'; const { ThemeProvider, StyleSheetManager, createGlobalStyle } = require('styled-components'); const bridge = require('@electron/remote').require('./bridge').default; diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/index.ts b/packages/app-desktop/gui/WindowCommandHandler/commands/index.ts index f238c44bd84..ea34d54b319 100644 --- a/packages/app-desktop/gui/WindowCommandHandler/commands/index.ts +++ b/packages/app-desktop/gui/WindowCommandHandler/commands/index.ts @@ -35,7 +35,6 @@ import * as showPrompt from './showPrompt'; import * as showShareFolderDialog from './showShareFolderDialog'; import * as showShareNoteDialog from './showShareNoteDialog'; import * as showSpellCheckerMenu from './showSpellCheckerMenu'; -import * as toggleEditors from './toggleEditors'; import * as toggleLayoutMoveMode from './toggleLayoutMoveMode'; import * as toggleMenuBar from './toggleMenuBar'; import * as toggleNoteList from './toggleNoteList'; @@ -83,7 +82,6 @@ const index: any[] = [ showShareFolderDialog, showShareNoteDialog, showSpellCheckerMenu, - toggleEditors, toggleLayoutMoveMode, toggleMenuBar, toggleNoteList, diff --git a/packages/lib/services/commands/ToolbarButtonUtils.ts b/packages/lib/services/commands/ToolbarButtonUtils.ts index 5097554d317..10e8366ea23 100644 --- a/packages/lib/services/commands/ToolbarButtonUtils.ts +++ b/packages/lib/services/commands/ToolbarButtonUtils.ts @@ -47,7 +47,9 @@ export default class ToolbarButtonUtils { return this.toolbarButtonCache_[commandName].info; } - const command = this.service.commandByName(commandName, { runtimeMustBeRegistered: true }); + // Don't force the command to have a registered runtime at this point -- if the command doesn't + // have a runtime, it will be disabled. + const command = this.service.commandByName(commandName); const output = { name: commandName, From 97904987b9455ed3d2798b4e660f5653b9e55da3 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Mon, 14 Oct 2024 11:38:50 -0700 Subject: [PATCH 22/97] Refactor NoteEditor to allow the "runtime must be registered" assertion to be restored By registering the toggleEditors runtime sooner, the `runtimeMustBeRegistered: true` assertion can be restored. --- .eslintignore | 2 ++ .gitignore | 2 ++ .../app-desktop/gui/NoteEditor/NoteEditor.tsx | 13 +++++---- packages/lib/hooks/useNowEffect.test.tsx | 29 +++++++++++++++++++ packages/lib/hooks/useNowEffect.ts | 28 ++++++++++++++++++ .../services/commands/ToolbarButtonUtils.ts | 4 +-- 6 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 packages/lib/hooks/useNowEffect.test.tsx create mode 100644 packages/lib/hooks/useNowEffect.ts diff --git a/.eslintignore b/.eslintignore index 06b147a84fb..d7139596d8e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -978,6 +978,8 @@ packages/lib/geolocation-node.js packages/lib/hooks/useAsyncEffect.js packages/lib/hooks/useElementSize.js packages/lib/hooks/useEventListener.js +packages/lib/hooks/useNowEffect.test.js +packages/lib/hooks/useNowEffect.js packages/lib/hooks/usePlugin.js packages/lib/hooks/usePrevious.js packages/lib/hooks/useQueuedAsyncEffect.test.js diff --git a/.gitignore b/.gitignore index 214adc89885..c7c17e69336 100644 --- a/.gitignore +++ b/.gitignore @@ -955,6 +955,8 @@ packages/lib/geolocation-node.js packages/lib/hooks/useAsyncEffect.js packages/lib/hooks/useElementSize.js packages/lib/hooks/useEventListener.js +packages/lib/hooks/useNowEffect.test.js +packages/lib/hooks/useNowEffect.js packages/lib/hooks/usePlugin.js packages/lib/hooks/usePrevious.js packages/lib/hooks/useQueuedAsyncEffect.test.js diff --git a/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx b/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx index 92fe0985b9e..3e6716e5a5d 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx @@ -7,6 +7,7 @@ import { AppState } from '../../app.reducer'; import { Dispatch } from 'redux'; import NewWindowOrIFrame from '../NewWindowOrIFrame'; import WindowCommandHandler from '../WindowCommandHandler/WindowCommandHandler'; +import useNowEffect from '@joplin/lib/hooks/useNowEffect'; import * as toggleEditors from './commands/toggleEditors'; @@ -39,24 +40,26 @@ interface UseToggleEditorsProps { isSafeMode: boolean; codeView: boolean; legacyMarkdown: boolean; - container: RefObject; + containerRef: RefObject; } const useToggleEditors = (props: UseToggleEditorsProps) => { const [codeView, setCodeView] = useState(props.codeView); - useEffect(() => { + // useLayoutEffect: Run the effect as soon as possible -- renders of child components (toolbar buttons) + // expect the command to have a registered runtime. + useNowEffect(() => { const runtime = toggleEditors.runtime(setCodeView); const registeredRuntime = CommandService.instance().registerRuntime( toggleEditors.declaration.name, - { ...runtime, getPriority: () => getWindowCommandPriority(props.container) }, + { ...runtime, getPriority: () => getWindowCommandPriority(props.containerRef) }, true, ); return () => { registeredRuntime.deregister(); }; - }, [props.container]); + }, [props.containerRef]); useEffect(() => { Setting.setValue('editor.codeView', codeView); @@ -79,7 +82,7 @@ const NoteEditorWrapper: React.FC = props => { const containerRef = useRef(null); const bodyEditor = useToggleEditors({ - isSafeMode: props.isSafeMode, codeView: props.codeView, legacyMarkdown: props.legacyMarkdown, container: containerRef, + isSafeMode: props.isSafeMode, codeView: props.codeView, legacyMarkdown: props.legacyMarkdown, containerRef: containerRef, }); const noteId = props.secondaryWindowNoteIds[props.windowId][0] ?? ''; const editor = noteId ?
    diff --git a/packages/lib/hooks/useNowEffect.test.tsx b/packages/lib/hooks/useNowEffect.test.tsx new file mode 100644 index 00000000000..3a43fd9b82f --- /dev/null +++ b/packages/lib/hooks/useNowEffect.test.tsx @@ -0,0 +1,29 @@ +import type * as React from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import useNowEffect from './useNowEffect'; + +describe('useNowEffect', () => { + test('should call the cleanup callback when the effect function is called and after unmount', async () => { + const cleanupFunction = jest.fn(() => {}); + const effectFunction = jest.fn(() => cleanupFunction); + const useTestHook = (dependencies: React.DependencyList) => { + return useNowEffect(effectFunction, dependencies); + }; + + const hook = renderHook(useTestHook, { initialProps: [0] }); + expect(cleanupFunction).not.toHaveBeenCalled(); + expect(effectFunction).toHaveBeenCalledTimes(1); + + hook.rerender([0]); + expect(cleanupFunction).not.toHaveBeenCalled(); + expect(effectFunction).toHaveBeenCalledTimes(1); + + hook.rerender([1]); + expect(cleanupFunction).toHaveBeenCalledTimes(1); + expect(effectFunction).toHaveBeenCalledTimes(2); + + hook.unmount(); + expect(cleanupFunction).toHaveBeenCalledTimes(2); + expect(effectFunction).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/lib/hooks/useNowEffect.ts b/packages/lib/hooks/useNowEffect.ts new file mode 100644 index 00000000000..c6752287792 --- /dev/null +++ b/packages/lib/hooks/useNowEffect.ts @@ -0,0 +1,28 @@ +import type * as React from 'react'; +import shim from '../shim'; + +type CleanupCallback = (()=> void)|null; +export type EffectFunction = ()=> CleanupCallback; +const { useRef, useMemo, useEffect } = shim.react(); + +// Like useEffect, but runs as soon as possible. +const useNowEffect = (effect: EffectFunction, dependencies: React.DependencyList) => { + const lastCleanup = useRef(null); + + const cleanupCallback = useMemo(() => { + lastCleanup.current?.(); + lastCleanup.current = null; + + return effect() ?? null; + // eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- This is a custom hook + }, dependencies); + lastCleanup.current = cleanupCallback; + + useEffect(() => { + return () => { + lastCleanup.current?.(); + lastCleanup.current = null; + }; + }, []); +}; +export default useNowEffect; diff --git a/packages/lib/services/commands/ToolbarButtonUtils.ts b/packages/lib/services/commands/ToolbarButtonUtils.ts index 10e8366ea23..5097554d317 100644 --- a/packages/lib/services/commands/ToolbarButtonUtils.ts +++ b/packages/lib/services/commands/ToolbarButtonUtils.ts @@ -47,9 +47,7 @@ export default class ToolbarButtonUtils { return this.toolbarButtonCache_[commandName].info; } - // Don't force the command to have a registered runtime at this point -- if the command doesn't - // have a runtime, it will be disabled. - const command = this.service.commandByName(commandName); + const command = this.service.commandByName(commandName, { runtimeMustBeRegistered: true }); const output = { name: commandName, From 74213e781c945de2c34717c420e448e22282bb21 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Tue, 15 Oct 2024 08:07:09 -0700 Subject: [PATCH 23/97] Fix context menus in secondary windows --- packages/app-desktop/ElectronAppWrapper.ts | 12 ++++-- packages/app-desktop/app.ts | 10 ++--- packages/app-desktop/bridge.ts | 42 ++++++++++--------- .../commands/replaceMisspelling.ts | 2 +- .../controls/plugins/PluginsStates.tsx | 2 +- packages/app-desktop/gui/MenuBar.tsx | 4 +- packages/app-desktop/gui/MultiNoteActions.tsx | 4 +- packages/app-desktop/gui/Navigator.tsx | 4 +- .../CodeMirror/utils/useContextMenu.ts | 14 ++++--- .../NoteBody/CodeMirror/v5/CodeMirror.tsx | 1 + .../NoteBody/CodeMirror/v6/CodeMirror.tsx | 1 + .../NoteBody/TinyMCE/utils/useContextMenu.ts | 23 +++++----- .../NoteListHeader/utils/useContextMenu.ts | 2 +- .../NoteListItem/utils/useOnContextMenu.ts | 2 +- packages/app-desktop/gui/PdfViewer.tsx | 2 +- packages/app-desktop/gui/Root.tsx | 4 +- .../gui/Sidebar/hooks/useOnRenderItem.tsx | 4 +- .../listItemComponents/AllNotesItem.tsx | 2 +- .../Sidebar/listItemComponents/HeaderItem.tsx | 2 +- .../commands/showSpellCheckerMenu.ts | 2 +- .../SpellCheckerServiceDriverNative.ts | 2 +- 21 files changed, 77 insertions(+), 64 deletions(-) diff --git a/packages/app-desktop/ElectronAppWrapper.ts b/packages/app-desktop/ElectronAppWrapper.ts index 360b1ff94fe..67750a9fac2 100644 --- a/packages/app-desktop/ElectronAppWrapper.ts +++ b/packages/app-desktop/ElectronAppWrapper.ts @@ -68,10 +68,14 @@ export default class ElectronAppWrapper { return this.logger_; } - public window() { + public mainWindow() { return this.win_; } + public activeWindow() { + return BrowserWindow.getFocusedWindow() ?? this.win_; + } + public env() { return this.env_; } @@ -445,11 +449,11 @@ export default class ElectronAppWrapper { this.tray_.setContextMenu(contextMenu); this.tray_.on('click', () => { - if (!this.window()) { + if (!this.mainWindow()) { console.warn('The window object was not available during the click event from tray icon'); return; } - this.window().show(); + this.mainWindow().show(); }); } catch (error) { console.error('Cannot create tray', error); @@ -476,7 +480,7 @@ export default class ElectronAppWrapper { // Someone tried to open a second instance - focus our window instead // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied this.electronApp_.on('second-instance', (_e: any, argv: string[]) => { - const win = this.window(); + const win = this.mainWindow(); if (!win) return; if (win.isMinimized()) win.restore(); win.show(); diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index 77bf3847dc6..341f531afdc 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -198,7 +198,7 @@ class Application extends BaseApplication { app.destroyTray(); } else { const contextMenu = Menu.buildFromTemplate([ - { label: _('Open %s', app.electronApp().name), click: () => { app.window().show(); } }, + { label: _('Open %s', app.electronApp().name), click: () => { app.mainWindow().show(); } }, { type: 'separator' }, { label: _('Quit'), click: () => { void app.quit(); } }, ]); @@ -407,7 +407,7 @@ class Application extends BaseApplication { if (Setting.value('sync.upgradeState') === Setting.SYNC_UPGRADE_STATE_MUST_DO) { reg.logger().info('app.start: doing upgradeSyncTarget action'); - bridge().window().show(); + bridge().mainWindow().show(); return { action: 'upgradeSyncTarget' }; } @@ -565,7 +565,7 @@ class Application extends BaseApplication { if (shim.isWindows() || shim.isMac()) { const runAutoUpdateCheck = () => { if (Setting.value('autoUpdateEnabled')) { - void checkForUpdates(true, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') }); + void checkForUpdates(true, bridge().mainWindow(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') }); } }; @@ -586,9 +586,9 @@ class Application extends BaseApplication { }, 1000 * 60 * 60); if (Setting.value('startMinimized') && Setting.value('showTrayIcon')) { - bridge().window().hide(); + bridge().mainWindow().hide(); } else { - bridge().window().show(); + bridge().mainWindow().show(); } void ShareService.instance().maintenance(); diff --git a/packages/app-desktop/bridge.ts b/packages/app-desktop/bridge.ts index a207d6e6c8d..af72ffce4c6 100644 --- a/packages/app-desktop/bridge.ts +++ b/packages/app-desktop/bridge.ts @@ -234,7 +234,7 @@ export class Bridge { // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied public setupContextMenu(_spellCheckerMenuItemsHandler: Function) { require('electron-context-menu')({ - allWindows: [this.window()], + allWindows: [this.mainWindow()], electronApp: this.electronApp(), @@ -259,8 +259,12 @@ export class Bridge { }); } - public window() { - return this.electronWrapper_.window(); + public mainWindow() { + return this.electronWrapper_.mainWindow(); + } + + public activeWindow() { + return this.electronWrapper_.activeWindow(); } public showItemInFolder(fullPath: string) { @@ -273,35 +277,35 @@ export class Bridge { } public windowContentSize() { - if (!this.window()) return { width: 0, height: 0 }; - const s = this.window().getContentSize(); + if (!this.mainWindow()) return { width: 0, height: 0 }; + const s = this.mainWindow().getContentSize(); return { width: s[0], height: s[1] }; } public windowSize() { - if (!this.window()) return { width: 0, height: 0 }; - const s = this.window().getSize(); + if (!this.mainWindow()) return { width: 0, height: 0 }; + const s = this.mainWindow().getSize(); return { width: s[0], height: s[1] }; } public windowSetSize(width: number, height: number) { - if (!this.window()) return; - return this.window().setSize(width, height); + if (!this.mainWindow()) return; + return this.mainWindow().setSize(width, height); } public openDevTools() { - return this.window().webContents.openDevTools(); + return this.mainWindow().webContents.openDevTools(); } public closeDevTools() { - return this.window().webContents.closeDevTools(); + return this.mainWindow().webContents.closeDevTools(); } // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied public async showSaveDialog(options: any) { if (!options) options = {}; if (!('defaultPath' in options) && this.lastSelectedPaths_.file) options.defaultPath = this.lastSelectedPaths_.file; - const { filePath } = await dialog.showSaveDialog(this.window(), options); + const { filePath } = await dialog.showSaveDialog(this.mainWindow(), options); if (filePath) { this.lastSelectedPaths_.file = filePath; } @@ -316,7 +320,7 @@ export class Bridge { if (!('defaultPath' in options) && (this.lastSelectedPaths_ as any)[fileType]) options.defaultPath = (this.lastSelectedPaths_ as any)[fileType]; if (!('createDirectory' in options)) options.createDirectory = true; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - const { filePaths } = await dialog.showOpenDialog(this.window(), options as any); + const { filePaths } = await dialog.showOpenDialog(this.mainWindow(), options as any); if (filePaths && filePaths.length) { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied (this.lastSelectedPaths_ as any)[fileType] = dirname(filePaths[0]); @@ -327,7 +331,7 @@ export class Bridge { // Don't use this directly - call one of the showXxxxxxxMessageBox() instead // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied private showMessageBox_(window: any, options: MessageDialogOptions): number { - if (!window) window = this.window(); + if (!window) window = this.mainWindow(); return dialog.showMessageBoxSync(window, { message: '', ...options }); } @@ -337,7 +341,7 @@ export class Bridge { ...options, }; - return this.showMessageBox_(this.window(), { + return this.showMessageBox_(this.mainWindow(), { type: 'error', message: message, buttons: options.buttons, @@ -350,7 +354,7 @@ export class Bridge { ...options, }; - const result = this.showMessageBox_(this.window(), { type: 'question', + const result = this.showMessageBox_(this.mainWindow(), { type: 'question', message: message, cancelId: 1, buttons: options.buttons, ...options }); @@ -360,7 +364,7 @@ export class Bridge { /* returns the index of the clicked button */ public showMessageBox(message: string, options: MessageDialogOptions = {}) { - const result = this.showMessageBox_(this.window(), { type: 'question', + const result = this.showMessageBox_(this.mainWindow(), { type: 'question', message: message, buttons: [_('OK'), _('Cancel')], ...options }); @@ -369,7 +373,7 @@ export class Bridge { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied public showInfoMessageBox(message: string, options: any = {}) { - const result = this.showMessageBox_(this.window(), { type: 'info', + const result = this.showMessageBox_(this.mainWindow(), { type: 'info', message: message, buttons: [_('OK')], ...options }); return result === 0; @@ -413,7 +417,7 @@ export class Bridge { const allowOpenId = 2; const learnMoreId = 1; const fileExtensionDescription = JSON.stringify(fileExtension); - const result = await dialog.showMessageBox(this.window(), { + const result = await dialog.showMessageBox(this.mainWindow(), { title: _('Unknown file type'), message: _('Joplin doesn\'t recognise the %s extension. Opening this file could be dangerous. What would you like to do?', fileExtensionDescription), diff --git a/packages/app-desktop/commands/replaceMisspelling.ts b/packages/app-desktop/commands/replaceMisspelling.ts index a1e0c7e436b..bf4f5659957 100644 --- a/packages/app-desktop/commands/replaceMisspelling.ts +++ b/packages/app-desktop/commands/replaceMisspelling.ts @@ -22,7 +22,7 @@ export const runtime = (): CommandRuntime => { if (!modalDialogVisible && (isInsideContainer(activeElement, 'codeMirrorEditor') || isInsideContainer(activeElement, 'tox-edit-area__iframe'))) { await CommandService.instance().execute('replaceSelection', suggestion); } else { - bridge().window().webContents.replaceMisspelling(suggestion); + bridge().mainWindow().webContents.replaceMisspelling(suggestion); } }, }; diff --git a/packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.tsx b/packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.tsx index 6e516597688..e4ecf74cae4 100644 --- a/packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.tsx +++ b/packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.tsx @@ -229,7 +229,7 @@ export default function(props: Props) { ]; const menu = bridge().Menu.buildFromTemplate(template); - menu.popup({ window: bridge().window() }); + menu.popup({ window: bridge().mainWindow() }); }, [onInstall, onBrowsePlugins]); const onSearchQueryChange = useCallback((event: OnChangeEvent) => { diff --git a/packages/app-desktop/gui/MenuBar.tsx b/packages/app-desktop/gui/MenuBar.tsx index 5f39fc90430..fa2d664e45d 100644 --- a/packages/app-desktop/gui/MenuBar.tsx +++ b/packages/app-desktop/gui/MenuBar.tsx @@ -196,7 +196,7 @@ const applyMenuBarVisibility = (showMenuBar: boolean) => { // The menu bar cannot be hidden on macOS if (shim.isMac()) return; - const window = bridge().window(); + const window = bridge().mainWindow(); window.setAutoHideMenuBar(!showMenuBar); window.setMenuBarVisibility(showMenuBar); }; @@ -579,7 +579,7 @@ function useMenu(props: Props) { if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) { ipcRenderer.send('check-for-updates'); } else { - void checkForUpdates(false, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') }); + void checkForUpdates(false, bridge().mainWindow(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') }); } } diff --git a/packages/app-desktop/gui/MultiNoteActions.tsx b/packages/app-desktop/gui/MultiNoteActions.tsx index c97bdf0f308..cd426f7518d 100644 --- a/packages/app-desktop/gui/MultiNoteActions.tsx +++ b/packages/app-desktop/gui/MultiNoteActions.tsx @@ -5,7 +5,7 @@ import { Dispatch } from 'redux'; import { ThemeStyle } from '@joplin/lib/theme'; import { buildStyle } from '@joplin/lib/theme'; -const bridge = require('@electron/remote').require('./bridge').default; +import bridge from '../services/bridge'; interface MultiNoteActionsProps { themeId: number; @@ -46,7 +46,7 @@ export default function MultiNoteActions(props: MultiNoteActionsProps) { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied const multiNotesButton_click = (item: any) => { if (item.submenu) { - item.submenu.popup({ window: bridge().window() }); + item.submenu.popup({ window: bridge().activeWindow() }); } else { item.click(); } diff --git a/packages/app-desktop/gui/Navigator.tsx b/packages/app-desktop/gui/Navigator.tsx index fcf6cc3eab8..5e8daeafdf6 100644 --- a/packages/app-desktop/gui/Navigator.tsx +++ b/packages/app-desktop/gui/Navigator.tsx @@ -2,7 +2,7 @@ const React = require('react'); const { connect } = require('react-redux'); import Setting from '@joplin/lib/models/Setting'; import { AppState } from '../app.reducer'; -const bridge = require('@electron/remote').require('./bridge').default; +import bridge from '../services/bridge'; interface Props { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied @@ -24,7 +24,7 @@ class NavigatorComponent extends React.Component { public updateWindowTitle(title: string) { try { - if (bridge().window()) bridge().window().setTitle(title); + if (bridge().mainWindow()) bridge().mainWindow().setTitle(title); } catch (error) { console.warn('updateWindowTitle', error); } diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.ts index ed6976260b5..3c8cc1716c7 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.ts +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.ts @@ -25,6 +25,7 @@ interface ContextMenuProps { editorPaste: ()=> void; editorRef: RefObject; editorClassName: string; + containerRef: RefObject; } const useContextMenu = (props: ContextMenuProps) => { @@ -51,12 +52,13 @@ const useContextMenu = (props: ContextMenuProps) => { function pointerInsideEditor(params: ContextMenuParams) { const x = params.x, y = params.y, isEditable = params.isEditable; - const elements = document.getElementsByClassName(props.editorClassName); + const containerDoc = props.containerRef.current?.ownerDocument; + const elements = containerDoc?.getElementsByClassName(props.editorClassName); // Note: We can't check inputFieldType here. When spellcheck is enabled, // params.inputFieldType is "none". When spellcheck is disabled, // params.inputFieldType is "plainText". Thus, such a check would be inconsistent. - if (!elements.length || !isEditable) return false; + if (!elements?.length || !isEditable) return false; // Checks whether the element the pointer clicked on is inside the editor. // This logic will need to be changed if the editor is eventually wrapped @@ -65,7 +67,7 @@ const useContextMenu = (props: ContextMenuProps) => { const zoom = Setting.value('windowContentZoomFactor'); const xScreen = convertFromScreenCoordinates(zoom, x); const yScreen = convertFromScreenCoordinates(zoom, y); - const intersectingElement = document.elementFromPoint(xScreen, yScreen); + const intersectingElement = containerDoc.elementFromPoint(xScreen, yScreen); return intersectingElement && isAncestorOfCodeMirrorEditor(intersectingElement); } @@ -155,13 +157,13 @@ const useContextMenu = (props: ContextMenuProps) => { // Prepend the event listener so that it gets called before // the listener that shows the default menu. - bridge().window().webContents.prependListener('context-menu', onContextMenu); + bridge().activeWindow().webContents.prependListener('context-menu', onContextMenu); return () => { - bridge().window().webContents.off('context-menu', onContextMenu); + bridge().activeWindow().webContents.off('context-menu', onContextMenu); }; }, [ - props.plugins, props.editorClassName, editorRef, + props.plugins, props.editorClassName, editorRef, props.containerRef, props.editorCutText, props.editorCopyText, props.editorPaste, ]); }; diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.tsx index 80acf4528c3..d0e7b8f9edc 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.tsx @@ -728,6 +728,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef(null); diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.ts index 5ced08d1cce..7804237a2ac 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.ts +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.ts @@ -12,17 +12,18 @@ import Setting from '@joplin/lib/models/Setting'; import Resource from '@joplin/lib/models/Resource'; import { TinyMceEditorEvents } from './types'; import { HtmlToMarkdownHandler, MarkupToHtmlHandler } from '../../../utils/types'; +import { Editor } from 'tinymce'; const menuUtils = new MenuUtils(CommandService.instance()); // x and y are the absolute coordinates, as returned by the context-menu event // handler on the webContent. This function will return null if the point is // not within the TinyMCE editor. -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied -function contextMenuElement(editor: any, x: number, y: number) { +function contextMenuElement(editor: Editor, x: number, y: number) { if (!editor || !editor.getDoc()) return null; - const iframes = document.getElementsByClassName('tox-edit-area__iframe'); + const containerDoc = editor.getContainer().ownerDocument; + const iframes = containerDoc.getElementsByClassName('tox-edit-area__iframe'); if (!iframes.length) return null; const zoom = Setting.value('windowContentZoomFactor') / 100; @@ -31,7 +32,7 @@ function contextMenuElement(editor: any, x: number, y: number) { // We use .elementFromPoint to handle the case where a dialog is covering // part of the editor. - const targetElement = document.elementFromPoint(xScreen, yScreen); + const targetElement = containerDoc.elementFromPoint(xScreen, yScreen); if (targetElement !== iframes[0]) { return null; } @@ -49,7 +50,7 @@ interface ContextMenuActionOptions { const contextMenuActionOptions: ContextMenuActionOptions = { current: null }; // eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied -export default function(editor: any, plugins: PluginStates, dispatch: Function, htmlToMd: HtmlToMarkdownHandler, mdToHtml: MarkupToHtmlHandler) { +export default function(editor: Editor, plugins: PluginStates, dispatch: Function, htmlToMd: HtmlToMarkdownHandler, mdToHtml: MarkupToHtmlHandler) { useEffect(() => { if (!editor) return () => {}; @@ -66,9 +67,9 @@ export default function(editor: any, plugins: PluginStates, dispatch: Function, if (element.nodeName === 'IMG') { itemType = ContextMenuItemType.Image; - resourceId = Resource.pathToId(element.src); + resourceId = Resource.pathToId((element as HTMLImageElement).src); } else if (element.nodeName === 'A') { - resourceId = Resource.pathToId(element.href); + resourceId = Resource.pathToId((element as HTMLAnchorElement).href); itemType = resourceId ? ContextMenuItemType.Resource : ContextMenuItemType.Link; linkToCopy = element.getAttribute('href') || ''; } else { @@ -118,14 +119,14 @@ export default function(editor: any, plugins: PluginStates, dispatch: Function, template = template.concat(menuUtils.pluginContextMenuItems(plugins, MenuItemLocation.EditorContextMenu)); const menu = bridge().Menu.buildFromTemplate(template); - menu.popup({ window: bridge().window() }); + menu.popup(); } - bridge().window().webContents.on('context-menu', onContextMenu); + bridge().activeWindow().webContents.on('context-menu', onContextMenu); return () => { - if (bridge().window()?.webContents?.off) { - bridge().window().webContents.off('context-menu', onContextMenu); + if (bridge().activeWindow()?.webContents?.off) { + bridge().activeWindow().webContents.off('context-menu', onContextMenu); } }; }, [editor, plugins, dispatch, htmlToMd, mdToHtml]); diff --git a/packages/app-desktop/gui/NoteListHeader/utils/useContextMenu.ts b/packages/app-desktop/gui/NoteListHeader/utils/useContextMenu.ts index 5411622d045..3d9c6ee8c04 100644 --- a/packages/app-desktop/gui/NoteListHeader/utils/useContextMenu.ts +++ b/packages/app-desktop/gui/NoteListHeader/utils/useContextMenu.ts @@ -46,6 +46,6 @@ export default (columns: NoteListColumns) => { const menu = Menu.buildFromTemplate(menuItems); - menu.popup({ window: bridge().window() }); + menu.popup({ window: bridge().mainWindow() }); }, [columns]); }; diff --git a/packages/app-desktop/gui/NoteListItem/utils/useOnContextMenu.ts b/packages/app-desktop/gui/NoteListItem/utils/useOnContextMenu.ts index 1579e2a67e4..6389733d916 100644 --- a/packages/app-desktop/gui/NoteListItem/utils/useOnContextMenu.ts +++ b/packages/app-desktop/gui/NoteListItem/utils/useOnContextMenu.ts @@ -50,7 +50,7 @@ const useOnContextMenu = ( customCss: customCss, }); - menu.popup({ window: bridge().window() }); + menu.popup({ window: bridge().mainWindow() }); }, [selectedNoteIds, notes, dispatch, watchedNoteFiles, plugins, selectedFolderId, customCss]); }; diff --git a/packages/app-desktop/gui/PdfViewer.tsx b/packages/app-desktop/gui/PdfViewer.tsx index a6a5ebebc66..d899341a2e8 100644 --- a/packages/app-desktop/gui/PdfViewer.tsx +++ b/packages/app-desktop/gui/PdfViewer.tsx @@ -69,7 +69,7 @@ export default function PdfViewer(props: Props) { mdToHtml: async (_a, b, _c) => { return { html: b, pluginAssets: [], cssStrings: [] }; }, } as ContextMenuOptions, props.dispatch); - menu.popup({ window: bridge().window() }); + menu.popup({ window: bridge().mainWindow() }); }, [props.dispatch]); // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied diff --git a/packages/app-desktop/gui/Root.tsx b/packages/app-desktop/gui/Root.tsx index a9b59031953..11736d18297 100644 --- a/packages/app-desktop/gui/Root.tsx +++ b/packages/app-desktop/gui/Root.tsx @@ -32,8 +32,8 @@ import JoplinCloudLoginScreen from './JoplinCloudLoginScreen'; import WindowCommandHandler from './WindowCommandHandler/WindowCommandHandler'; import NoteEditorWrapper from './NoteEditor/NoteEditor'; import { stateUtils, WindowState } from '@joplin/lib/reducer'; +import bridge from '../services/bridge'; const { ThemeProvider, StyleSheetManager, createGlobalStyle } = require('styled-components'); -const bridge = require('@electron/remote').require('./bridge').default; interface Props { themeId: number; @@ -105,7 +105,7 @@ const GlobalStyle = createGlobalStyle` let wcsTimeoutId_: any = null; async function initialize() { - bridge().window().on('resize', () => { + bridge().activeWindow().on('resize', () => { if (wcsTimeoutId_) shim.clearTimeout(wcsTimeoutId_); wcsTimeoutId_ = shim.setTimeout(() => { diff --git a/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx b/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx index aad5bf709e7..67f13043c09 100644 --- a/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx +++ b/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx @@ -104,7 +104,7 @@ const useOnRenderItem = (props: Props) => { menu.append( new MenuItem(menuUtils.commandToStatefulMenuItem('emptyTrash')), ); - menu.popup({ window: bridge().window() }); + menu.popup({ window: bridge().mainWindow() }); return; } @@ -254,7 +254,7 @@ const useOnRenderItem = (props: Props) => { } } - menu.popup({ window: bridge().window() }); + menu.popup({ window: bridge().mainWindow() }); }, [props.dispatch, pluginsRef]); diff --git a/packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.tsx b/packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.tsx index 70a5e51307c..3cb2f5997fc 100644 --- a/packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.tsx +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.tsx @@ -42,7 +42,7 @@ const AllNotesItem: React.FC = props => { })); } - menu.popup({ window: bridge().window() }); + menu.popup({ window: bridge().mainWindow() }); }, []); return ( diff --git a/packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.tsx b/packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.tsx index 357a17a7ead..6f1aa5b4cee 100644 --- a/packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.tsx +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.tsx @@ -38,7 +38,7 @@ const HeaderItem: React.FC = props => { new MenuItem(menuUtils.commandToStatefulMenuItem('newFolder')), ); - menu.popup({ window: bridge().window() }); + menu.popup({ window: bridge().mainWindow() }); } }, [itemId]); diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/showSpellCheckerMenu.ts b/packages/app-desktop/gui/WindowCommandHandler/commands/showSpellCheckerMenu.ts index 73476bf687c..83dce6c7b06 100644 --- a/packages/app-desktop/gui/WindowCommandHandler/commands/showSpellCheckerMenu.ts +++ b/packages/app-desktop/gui/WindowCommandHandler/commands/showSpellCheckerMenu.ts @@ -21,7 +21,7 @@ export const runtime = (): CommandRuntime => { const menuItems = SpellCheckerService.instance().spellCheckerConfigMenuItems(selectedLanguages, useSpellChecker); // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied const menu = Menu.buildFromTemplate(menuItems as any); - menu.popup({ window: bridge().window() }); + menu.popup({ window: bridge().mainWindow() }); }, mapStateToTitle(state: AppState): string { diff --git a/packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.ts b/packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.ts index ce933fef2ba..e9cdae75870 100644 --- a/packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.ts +++ b/packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.ts @@ -11,7 +11,7 @@ export default class SpellCheckerServiceDriverNative extends SpellCheckerService // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied private session(): any { - return bridge().window().webContents.session; + return bridge().mainWindow().webContents.session; } public get availableLanguages(): string[] { From 4179b4f8d32a3bfec3403314750e2a72133a5720 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Tue, 15 Oct 2024 10:38:49 -0700 Subject: [PATCH 24/97] Make background toolbar buttons be enabled/disabled based on their container window and fix tag display --- .../app-desktop/gui/NewWindowOrIFrame.tsx | 20 ++- .../NoteBody/CodeMirror/Toolbar.tsx | 8 +- .../NoteBody/CodeMirror/v5/CodeMirror.tsx | 7 +- .../NoteBody/CodeMirror/v6/CodeMirror.tsx | 7 +- .../app-desktop/gui/NoteEditor/NoteEditor.tsx | 13 +- .../gui/NoteEditor/NoteEditorContent.tsx | 29 ++-- .../gui/NoteEditor/NoteTitle/NoteTitleBar.tsx | 6 +- .../gui/NoteToolbar/NoteToolbar.tsx | 11 +- packages/lib/reducer.test.ts | 93 ++++++++++++- packages/lib/reducer.ts | 130 ++++++++++-------- .../commands/stateToWhenClauseContext.ts | 20 +-- 11 files changed, 241 insertions(+), 103 deletions(-) diff --git a/packages/app-desktop/gui/NewWindowOrIFrame.tsx b/packages/app-desktop/gui/NewWindowOrIFrame.tsx index 83949aca28c..b8fd5e029b2 100644 --- a/packages/app-desktop/gui/NewWindowOrIFrame.tsx +++ b/packages/app-desktop/gui/NewWindowOrIFrame.tsx @@ -1,13 +1,20 @@ import shim from '@joplin/lib/shim'; import * as React from 'react'; -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, createContext } from 'react'; import { createPortal } from 'react-dom'; +export enum WindowMode { + Iframe, NewWindow, +} + +export const WindowIdContext = createContext('default'); + interface Props { // Note: children will be rendered in a different DOM from this node. Avoid using document.* methods // in child components. children: React.ReactNode[]|React.ReactNode; - newWindow: boolean; + mode: WindowMode; + windowId: string; onClose: ()=> void; onFocus?: ()=> void; } @@ -27,7 +34,7 @@ const NewWindowOrIFrame: React.FC = props => { const unmounted = false; if (iframeRef) { setDoc(iframeRef?.contentWindow?.document); - } else if (props.newWindow) { + } else if (props.mode === WindowMode.NewWindow) { openedWindow = window.open('about:blank'); setDoc(openedWindow.document); @@ -62,7 +69,7 @@ const NewWindowOrIFrame: React.FC = props => { onCloseRef.current?.(); } }; - }, [iframeRef, props.newWindow]); + }, [iframeRef, props.mode]); useEffect(() => { if (!doc) return; @@ -101,10 +108,11 @@ const NewWindowOrIFrame: React.FC = props => { setLoaded(true); }, [doc]); const parentNode = loaded ? doc?.body : null; + const wrappedChildren = {props.children}; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Needed to allow adding the portal to the DOM - const contentPortal = parentNode && createPortal(props.children, parentNode) as any; - if (props.newWindow) { + const contentPortal = parentNode && createPortal(wrappedChildren, parentNode) as any; + if (props.mode === WindowMode.NewWindow) { return
    {contentPortal}
    ; } else { return - ); - } -} + containerWindow?.removeEventListener('message', messageListener); + + removePluginAssetsCallbackRef.current?.(); + }; + }, [containerWindow]); + + const viewerStyle = useMemo(() => { + return { border: 'none', ...props.viewerStyle }; + }, [props.viewerStyle]); + + // allow=fullscreen: Required to allow the user to fullscreen videos. + return ( + + ); +}); + +export default NoteTextViewer; diff --git a/packages/app-desktop/services/plugins/UserWebview.tsx b/packages/app-desktop/services/plugins/UserWebview.tsx index fd41426093c..a9ed3d5b9cc 100644 --- a/packages/app-desktop/services/plugins/UserWebview.tsx +++ b/packages/app-desktop/services/plugins/UserWebview.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useRef, useImperativeHandle, forwardRef, useEffect, useMemo } from 'react'; +import { useRef, useImperativeHandle, forwardRef, useEffect, useMemo, useContext } from 'react'; import useViewIsReady from './hooks/useViewIsReady'; import useThemeCss from './hooks/useThemeCss'; import useContentSize from './hooks/useContentSize'; @@ -9,6 +9,7 @@ import useWebviewToPluginMessages from './hooks/useWebviewToPluginMessages'; import useScriptLoader from './hooks/useScriptLoader'; import Logger from '@joplin/utils/Logger'; import { focus } from '@joplin/lib/utils/focusHandler'; +import { WindowIdContext } from '../../gui/NewWindowOrIFrame'; const logger = Logger.create('UserWebview'); @@ -125,11 +126,13 @@ function UserWebview(props: Props, ref: any) { htmlHash, ); + const windowId = useContext(WindowIdContext); useWebviewToPluginMessages( frameWindow(), isReady, props.pluginId, props.viewId, + windowId, postMessage, ); diff --git a/packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.ts b/packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.ts index 3ebfb3c0621..cb4be9cadc0 100644 --- a/packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.ts +++ b/packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.ts @@ -2,14 +2,14 @@ import PostMessageService, { MessageResponse, ResponderComponentType } from '@jo import { useEffect } from 'react'; // eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied -export default function(frameWindow: any, isReady: boolean, pluginId: string, viewId: string, postMessage: Function) { +export default function(frameWindow: any, isReady: boolean, pluginId: string, viewId: string, windowId: string, postMessage: Function) { useEffect(() => { - PostMessageService.instance().registerResponder(ResponderComponentType.UserWebview, viewId, (message: MessageResponse) => { + PostMessageService.instance().registerResponder(ResponderComponentType.UserWebview, viewId, windowId, (message: MessageResponse) => { postMessage('postMessageService.response', { message }); }); return () => { - PostMessageService.instance().unregisterResponder(ResponderComponentType.UserWebview, viewId); + PostMessageService.instance().unregisterResponder(ResponderComponentType.UserWebview, viewId, windowId); }; // eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied }, [viewId]); diff --git a/packages/lib/services/PostMessageService.ts b/packages/lib/services/PostMessageService.ts index c383d5de5ab..5172eea2429 100644 --- a/packages/lib/services/PostMessageService.ts +++ b/packages/lib/services/PostMessageService.ts @@ -53,6 +53,7 @@ type ViewMessageHandler = (message: any)=> void; interface Message { pluginId: string; + windowId: string; contentScriptId: string; viewId: string; from: MessageParticipant; @@ -120,9 +121,9 @@ export default class PostMessageService { let responder: MessageResponder = null; if (message.from === MessageParticipant.ContentScript) { - responder = this.responder(ResponderComponentType.NoteTextViewer, message.viewId); + responder = this.responder(ResponderComponentType.NoteTextViewer, message.viewId, message.windowId); } else if (message.from === MessageParticipant.UserWebview) { - responder = this.responder(ResponderComponentType.UserWebview, message.viewId); + responder = this.responder(ResponderComponentType.UserWebview, message.viewId, message.windowId); } if (!responder) { @@ -136,13 +137,16 @@ export default class PostMessageService { }); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - private responder(type: ResponderComponentType, viewId: string): any { - return this.responders_[[type, viewId].join(':')]; + private responder(type: ResponderComponentType, viewId: string, windowId: string) { + return this.responders_[[type, viewId, windowId].join(':')]; + } + + public registerResponder(type: ResponderComponentType, viewId: string, windowId: string, responder: MessageResponder) { + this.responders_[[type, viewId, windowId].join(':')] = responder; } - public registerResponder(type: ResponderComponentType, viewId: string, responder: MessageResponder) { - this.responders_[[type, viewId].join(':')] = responder; + public unregisterResponder(type: ResponderComponentType, viewId: string, windowId: string) { + delete this.responders_[[type, viewId, windowId].join(':')]; } public registerViewMessageHandler(type: ResponderComponentType, viewId: string, callback: ViewMessageHandler) { @@ -153,8 +157,4 @@ export default class PostMessageService { delete this.viewMessageHandlers_[[type, viewId].join(':')]; } - public unregisterResponder(type: ResponderComponentType, viewId: string) { - delete this.responders_[[type, viewId].join(':')]; - } - } diff --git a/packages/lib/services/plugins/WebviewController.ts b/packages/lib/services/plugins/WebviewController.ts index 87e729854bf..5bc25c96c6e 100644 --- a/packages/lib/services/plugins/WebviewController.ts +++ b/packages/lib/services/plugins/WebviewController.ts @@ -4,6 +4,7 @@ import { ButtonSpec, DialogResult, ViewHandle } from './api/types'; const { toSystemSlashes } = require('../../path-utils'); import PostMessageService, { MessageParticipant } from '../PostMessageService'; import { PluginViewState } from './reducer'; +import { defaultWindowId } from '../../reducer'; export enum ContainerType { Panel = 'panel', @@ -124,6 +125,7 @@ export default class WebviewController extends ViewController { void PostMessageService.instance().postMessage({ pluginId: this.pluginId, viewId: this.handle, + windowId: defaultWindowId, contentScriptId: null, from: MessageParticipant.Plugin, to: MessageParticipant.UserWebview, From 6c23455939072cc8e17135d67dc636661e3006d1 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 31 Oct 2024 14:12:14 -0700 Subject: [PATCH 54/97] Fix error on NoteTextViewer component unmount --- packages/app-desktop/gui/NoteTextViewer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-desktop/gui/NoteTextViewer.tsx b/packages/app-desktop/gui/NoteTextViewer.tsx index edd7061e82d..ff0d0e677b6 100644 --- a/packages/app-desktop/gui/NoteTextViewer.tsx +++ b/packages/app-desktop/gui/NoteTextViewer.tsx @@ -198,7 +198,7 @@ const NoteTextViewer = forwardRef((props: Props, ref: ForwardedRef Date: Thu, 31 Oct 2024 14:19:05 -0700 Subject: [PATCH 55/97] Attempting to fix dialog messaging --- .../services/plugins/hooks/useWebviewToPluginMessages.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.ts b/packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.ts index cb4be9cadc0..aacf8758f5e 100644 --- a/packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.ts +++ b/packages/app-desktop/services/plugins/hooks/useWebviewToPluginMessages.ts @@ -32,6 +32,7 @@ export default function(frameWindow: any, isReady: boolean, pluginId: string, vi void PostMessageService.instance().postMessage({ pluginId, viewId, + windowId, ...event.data.message, }); } @@ -43,5 +44,5 @@ export default function(frameWindow: any, isReady: boolean, pluginId: string, vi if (frameWindow?.removeEventListener) frameWindow.removeEventListener('message', onMessage_); }; // eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied - }, [frameWindow, isReady, pluginId, viewId]); + }, [frameWindow, isReady, pluginId, windowId, viewId]); } From b4ee6649a661959418ea53bf23b2540173b81d4f Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 31 Oct 2024 17:27:21 -0700 Subject: [PATCH 56/97] Improve menubar/navigation --- .eslintignore | 1 + .gitignore | 1 + packages/app-desktop/ElectronAppWrapper.ts | 29 +++++++++++- packages/app-desktop/bridge.ts | 8 ++++ packages/app-desktop/gui/MenuBar.tsx | 47 ++++++++++++++----- packages/app-desktop/gui/Navigator.tsx | 21 +++++++-- .../app-desktop/gui/NewWindowOrIFrame.tsx | 9 ++++ .../gui/NoteEditor/EditorWindow.tsx | 2 + packages/app-desktop/gui/Root.tsx | 2 +- .../utils/window/secondaryWindowPreload.js | 5 ++ packages/app-desktop/utils/window/types.ts | 4 ++ 11 files changed, 112 insertions(+), 17 deletions(-) create mode 100644 packages/app-desktop/utils/window/secondaryWindowPreload.js create mode 100644 packages/app-desktop/utils/window/types.ts diff --git a/.eslintignore b/.eslintignore index 1251d352294..290531de919 100644 --- a/.eslintignore +++ b/.eslintignore @@ -552,6 +552,7 @@ packages/app-desktop/utils/isSafeToOpen.test.js packages/app-desktop/utils/isSafeToOpen.js packages/app-desktop/utils/restartInSafeModeFromMain.test.js packages/app-desktop/utils/restartInSafeModeFromMain.js +packages/app-desktop/utils/window/types.js packages/app-mobile/PluginAssetsLoader.js packages/app-mobile/commands/index.js packages/app-mobile/commands/newNote.test.js diff --git a/.gitignore b/.gitignore index 587245d609d..c548051646b 100644 --- a/.gitignore +++ b/.gitignore @@ -529,6 +529,7 @@ packages/app-desktop/utils/isSafeToOpen.test.js packages/app-desktop/utils/isSafeToOpen.js packages/app-desktop/utils/restartInSafeModeFromMain.test.js packages/app-desktop/utils/restartInSafeModeFromMain.js +packages/app-desktop/utils/window/types.js packages/app-mobile/PluginAssetsLoader.js packages/app-mobile/commands/index.js packages/app-mobile/commands/newNote.test.js diff --git a/packages/app-desktop/ElectronAppWrapper.ts b/packages/app-desktop/ElectronAppWrapper.ts index 67750a9fac2..57add4507f3 100644 --- a/packages/app-desktop/ElectronAppWrapper.ts +++ b/packages/app-desktop/ElectronAppWrapper.ts @@ -17,6 +17,8 @@ import { _ } from '@joplin/lib/locale'; import restartInSafeModeFromMain from './utils/restartInSafeModeFromMain'; import handleCustomProtocols, { CustomProtocolHandler } from './utils/customProtocols/handleCustomProtocols'; import { clearTimeout, setTimeout } from 'timers'; +import { resolve } from 'path'; +import { defaultWindowId } from '@joplin/lib/reducer'; interface RendererProcessQuitReply { canClose: boolean; @@ -42,6 +44,7 @@ export default class ElectronAppWrapper { private buildDir_: string = null; private rendererProcessQuitReply_: RendererProcessQuitReply = null; private pluginWindows_: PluginWindows = {}; + private joplinWindowIdToElectronId_: Map = new Map(); private initialCallbackUrl_: string = null; private updaterService_: AutoUpdaterService = null; private customProtocolHandler_: CustomProtocolHandler = null; @@ -76,6 +79,18 @@ export default class ElectronAppWrapper { return BrowserWindow.getFocusedWindow() ?? this.win_; } + public windowById(joplinId: string) { + if (joplinId === defaultWindowId) { + return this.mainWindow(); + } + + const electronId = this.joplinWindowIdToElectronId_.get(joplinId); + if (electronId !== undefined) { + return BrowserWindow.fromId(electronId); + } + return null; + } + public env() { return this.env_; } @@ -254,7 +269,9 @@ export default class ElectronAppWrapper { return { action: 'allow', overrideBrowserWindowOptions: { - webPreferences: { }, + webPreferences: { + preload: resolve(__dirname, './utils/window/secondaryWindowPreload.js'), + }, }, }; } else if (event.url.match(/^https?:\/\//)) { @@ -364,6 +381,16 @@ export default class ElectronAppWrapper { void this.updaterService_.checkForUpdates(true); }); + ipcMain.on('secondary-window-added', (event, windowId: string) => { + const window = BrowserWindow.fromWebContents(event.sender); + const electronWindowId = window?.id; + this.joplinWindowIdToElectronId_.set(windowId, electronWindowId); + + window.once('close', () => { + this.joplinWindowIdToElectronId_.delete(windowId); + }); + }); + // Let us register listeners on the window, so we can update the state // automatically (the listeners will be removed when the window is closed) // and restore the maximized or full screen state diff --git a/packages/app-desktop/bridge.ts b/packages/app-desktop/bridge.ts index 7ed3658de10..a1339de9b41 100644 --- a/packages/app-desktop/bridge.ts +++ b/packages/app-desktop/bridge.ts @@ -267,6 +267,14 @@ export class Bridge { return this.electronWrapper_.activeWindow(); } + public hasActiveWindow() { + return !!BrowserWindow.getFocusedWindow(); + } + + public windowById(id: string) { + return this.electronWrapper_.windowById(id); + } + public showItemInFolder(fullPath: string) { return require('electron').shell.showItemInFolder(toSystemSlashes(fullPath)); } diff --git a/packages/app-desktop/gui/MenuBar.tsx b/packages/app-desktop/gui/MenuBar.tsx index 8ad0e73b53c..276618a9bb3 100644 --- a/packages/app-desktop/gui/MenuBar.tsx +++ b/packages/app-desktop/gui/MenuBar.tsx @@ -1,7 +1,7 @@ import { useEffect, useState, useRef, useCallback, useMemo } from 'react'; import { AppState } from '../app.reducer'; import InteropService from '@joplin/lib/services/interop/InteropService'; -import { stateUtils } from '@joplin/lib/reducer'; +import { defaultWindowId, stateUtils } from '@joplin/lib/reducer'; import CommandService from '@joplin/lib/services/CommandService'; import MenuUtils from '@joplin/lib/services/commands/MenuUtils'; import KeymapService from '@joplin/lib/services/KeymapService'; @@ -19,7 +19,7 @@ import menuCommandNames from './menuCommandNames'; import stateToWhenClauseContext from '../services/commands/stateToWhenClauseContext'; import bridge from '../services/bridge'; import checkForUpdates from '../checkForUpdates'; -const { connect } = require('react-redux'); +import { connect } from 'react-redux'; import { reg } from '@joplin/lib/registry'; import { ProfileConfig } from '@joplin/lib/services/profileConfig/types'; import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService'; @@ -150,7 +150,7 @@ interface Props { dispatch: Function; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied menuItemProps: any; - routeName: string; + mainScreenVisible: boolean; selectedFolderId: string; layoutButtonSequence: number; ['notes.sortOrder.field']: string; @@ -173,6 +173,8 @@ interface Props { pluginSettings: PluginSettings; noteListRendererIds: string[]; noteListRendererId: string; + isWindowFocused: boolean; + windowId: string; showMenuBar: boolean; } @@ -192,11 +194,11 @@ function menuItemSetEnabled(id: string, enabled: boolean) { menuItem.enabled = enabled; } -const applyMenuBarVisibility = (showMenuBar: boolean) => { +const applyMenuBarVisibility = (windowId: string, showMenuBar: boolean) => { // The menu bar cannot be hidden on macOS if (shim.isMac()) return; - const window = bridge().mainWindow(); + const window = bridge().windowById(windowId) ?? bridge().mainWindow(); window.setAutoHideMenuBar(!showMenuBar); window.setMenuBarVisibility(showMenuBar); }; @@ -1019,7 +1021,7 @@ function useMenu(props: Props) { rootMenus.help, ].filter(item => item !== null); - if (props.routeName !== 'Main') { + if (!props.mainScreenVisible) { setMenu(Menu.buildFromTemplate([ { label: _('&File'), @@ -1049,7 +1051,7 @@ function useMenu(props: Props) { }; // eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied }, [ - props.routeName, + props.mainScreenVisible, props.pluginMenuItems, props.pluginMenus, keymapLastChangeTime, @@ -1099,18 +1101,41 @@ function useMenu(props: Props) { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied function MenuBar(props: Props): any { const menu = useMenu(props); - if (menu) Menu.setApplicationMenu(menu); - applyMenuBarVisibility(props.showMenuBar); + + useEffect(() => { + if (shim.isMac()) { + // On MacOS, window.setMenu does nothing and the app menu must be set instead + if (props.isWindowFocused) { + Menu.setApplicationMenu(menu); + } + } else { + bridge().windowById(props.windowId)?.setMenu(menu); + } + }, [menu, props.windowId, props.isWindowFocused]); + + useEffect(() => { + applyMenuBarVisibility(props.windowId, props.showMenuBar); + }, [props.showMenuBar, props.windowId]); + return null; } -const mapStateToProps = (state: AppState) => { +interface OwnProps { + windowId: string; +} + +const mapStateToProps = (state: AppState, { windowId }: OwnProps) => { const whenClauseContext = stateToWhenClauseContext(state); + const isSecondaryWindow = windowId !== defaultWindowId; + return { + windowId: windowId, menuItemProps: menuUtils.commandsToMenuItemProps(commandNames.concat(getPluginCommandNames(state.pluginService.plugins)), whenClauseContext), locale: state.settings.locale, - routeName: state.route.routeName, + // Secondary windows can only show the main screen + mainScreenVisible: state.route.routeName === 'Main' || isSecondaryWindow, + isWindowFocused: windowId === state.windowId, selectedFolderId: state.selectedFolderId, layoutButtonSequence: state.settings.layoutButtonSequence, ['notes.sortOrder.field']: state.settings['notes.sortOrder.field'], diff --git a/packages/app-desktop/gui/Navigator.tsx b/packages/app-desktop/gui/Navigator.tsx index 5e8daeafdf6..3b16c9870dc 100644 --- a/packages/app-desktop/gui/Navigator.tsx +++ b/packages/app-desktop/gui/Navigator.tsx @@ -1,24 +1,37 @@ -const React = require('react'); +import * as React from 'react'; const { connect } = require('react-redux'); import Setting from '@joplin/lib/models/Setting'; import { AppState } from '../app.reducer'; import bridge from '../services/bridge'; +import { focus } from '@joplin/lib/utils/focusHandler'; interface Props { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied route: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + screens: Record; + + style: React.CSSProperties; + className?: string; } class NavigatorComponent extends React.Component { - public UNSAFE_componentWillReceiveProps(newProps: Props) { - if (newProps.route) { - const screenInfo = this.props.screens[newProps.route.routeName]; + public override componentDidUpdate(prevProps: Props): void { + if (this.props.route) { + const routeName = this.props.route.routeName; + const screenInfo = this.props.screens[routeName]; const devMarker = Setting.value('env') === 'dev' ? ` (DEV - ${Setting.value('profileDir')})` : ''; const windowTitle = [`Joplin${devMarker}`]; if (screenInfo.title) { windowTitle.push(screenInfo.title()); } this.updateWindowTitle(windowTitle.join(' - ')); + + if (prevProps.route.routeName && routeName !== prevProps.route.routeName) { + if (bridge().hasActiveWindow() && bridge().activeWindow() !== bridge().mainWindow()) { + focus('Navigator', bridge().mainWindow()); + } + } } } diff --git a/packages/app-desktop/gui/NewWindowOrIFrame.tsx b/packages/app-desktop/gui/NewWindowOrIFrame.tsx index 5b2719187a4..dfae971ebd7 100644 --- a/packages/app-desktop/gui/NewWindowOrIFrame.tsx +++ b/packages/app-desktop/gui/NewWindowOrIFrame.tsx @@ -3,6 +3,7 @@ import shim from '@joplin/lib/shim'; import * as React from 'react'; import { useState, useEffect, useRef, createContext } from 'react'; import { createPortal } from 'react-dom'; +import { SecondaryWindowApi } from '../utils/window/types'; export enum WindowMode { Iframe, NewWindow, @@ -115,6 +116,14 @@ const NewWindowOrIFrame: React.FC = props => { doc.title = props.title; }, [doc, props.title]); + useEffect(() => { + const win = doc?.defaultView; + if (win && 'electronWindow' in win && typeof win.electronWindow === 'object') { + const electronWindow = win.electronWindow as SecondaryWindowApi; + electronWindow.onSetWindowId(props.windowId); + } + }, [doc, props.windowId]); + const parentNode = loaded ? doc?.body : null; const wrappedChildren = {props.children}; diff --git a/packages/app-desktop/gui/NoteEditor/EditorWindow.tsx b/packages/app-desktop/gui/NoteEditor/EditorWindow.tsx index 8ee58aa087d..380a643d0ef 100644 --- a/packages/app-desktop/gui/NoteEditor/EditorWindow.tsx +++ b/packages/app-desktop/gui/NoteEditor/EditorWindow.tsx @@ -13,6 +13,7 @@ const { StyleSheetManager } = require('styled-components'); import createCache from '@emotion/cache'; import { CacheProvider } from '@emotion/react'; import { stateUtils } from '@joplin/lib/reducer'; +import MenuBar from '../MenuBar'; interface Props { dispatch: Dispatch; @@ -71,6 +72,7 @@ const SecondaryWindow: React.FC = props => { title={windowTitle} > + {editor} diff --git a/packages/app-desktop/gui/Root.tsx b/packages/app-desktop/gui/Root.tsx index 34a09b3f519..0e5185dee84 100644 --- a/packages/app-desktop/gui/Root.tsx +++ b/packages/app-desktop/gui/Root.tsx @@ -257,7 +257,7 @@ class RootComponent extends React.Component { - + diff --git a/packages/app-desktop/utils/window/secondaryWindowPreload.js b/packages/app-desktop/utils/window/secondaryWindowPreload.js new file mode 100644 index 00000000000..866ffd4cc97 --- /dev/null +++ b/packages/app-desktop/utils/window/secondaryWindowPreload.js @@ -0,0 +1,5 @@ +const { ipcRenderer } = require('electron'); + +window.electronWindow = { + onSetWindowId: windowId => ipcRenderer.send('secondary-window-added', windowId), +}; diff --git a/packages/app-desktop/utils/window/types.ts b/packages/app-desktop/utils/window/types.ts new file mode 100644 index 00000000000..045b2829394 --- /dev/null +++ b/packages/app-desktop/utils/window/types.ts @@ -0,0 +1,4 @@ + +export interface SecondaryWindowApi { + onSetWindowId(windowId: string): void; +} From 29bb7d2c6d8561127e64c6f6b4df1f701bc0fa12 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 31 Oct 2024 17:37:34 -0700 Subject: [PATCH 57/97] Move main-window focusing logic to MenuBar --- packages/app-desktop/gui/MenuBar.tsx | 37 +++++++++++++++----------- packages/app-desktop/gui/Navigator.tsx | 9 +------ packages/lib/BaseApplication.ts | 2 ++ 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/app-desktop/gui/MenuBar.tsx b/packages/app-desktop/gui/MenuBar.tsx index 276618a9bb3..1a6bd1297a1 100644 --- a/packages/app-desktop/gui/MenuBar.tsx +++ b/packages/app-desktop/gui/MenuBar.tsx @@ -27,6 +27,11 @@ import { getListRendererById, getListRendererIds } from '@joplin/lib/services/no import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect'; import { EventName } from '@joplin/lib/eventManager'; import { ipcRenderer } from 'electron'; +import NavService from '@joplin/lib/services/NavService'; +import Logger from '@joplin/utils/Logger'; + +const logger = Logger.create('MenuBar'); + const packageInfo: PackageInfo = require('../packageInfo.js'); const { clipboard } = require('electron'); const Menu = bridge().Menu; @@ -404,6 +409,17 @@ function useMenu(props: Props) { const keymapService = KeymapService.instance(); + const navigateTo = (routeName: string) => { + void NavService.go(routeName); + + // NavService.go opens in the main window -- switch to it to show the screen: + const isBackgroundWindow = props.windowId !== defaultWindowId; + if (isBackgroundWindow) { + logger.info('Focusing the main window'); + bridge().mainWindow().show(); + } + }; + const quitMenuItem = { label: _('Quit'), accelerator: keymapService.getAccelerator('quit'), @@ -517,10 +533,7 @@ function useMenu(props: Props) { const syncStatusItem = { label: _('Synchronisation Status'), click: () => { - props.dispatch({ - type: 'NAV_GO', - routeName: 'Status', - }); + navigateTo('Status'); }, }; @@ -550,10 +563,7 @@ function useMenu(props: Props) { label: _('Options'), accelerator: keymapService.getAccelerator('config'), click: () => { - props.dispatch({ - type: 'NAV_GO', - routeName: 'Config', - }); + navigateTo('Config'); }, }, separator(), @@ -563,10 +573,7 @@ function useMenu(props: Props) { const toolsItemsAll = [{ label: _('Note attachments...'), click: () => { - props.dispatch({ - type: 'NAV_GO', - routeName: 'Resources', - }); + navigateTo('Resources'); }, }]; @@ -621,10 +628,7 @@ function useMenu(props: Props) { visible: !!shim.isMac(), accelerator: shim.isMac() && keymapService.getAccelerator('config'), click: () => { - props.dispatch({ - type: 'NAV_GO', - routeName: 'Config', - }); + navigateTo('Config'); }, }, { label: _('Check for updates...'), @@ -1051,6 +1055,7 @@ function useMenu(props: Props) { }; // eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied }, [ + props.windowId, props.mainScreenVisible, props.pluginMenuItems, props.pluginMenus, diff --git a/packages/app-desktop/gui/Navigator.tsx b/packages/app-desktop/gui/Navigator.tsx index 3b16c9870dc..9a757721c0f 100644 --- a/packages/app-desktop/gui/Navigator.tsx +++ b/packages/app-desktop/gui/Navigator.tsx @@ -3,7 +3,6 @@ const { connect } = require('react-redux'); import Setting from '@joplin/lib/models/Setting'; import { AppState } from '../app.reducer'; import bridge from '../services/bridge'; -import { focus } from '@joplin/lib/utils/focusHandler'; interface Props { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied @@ -16,7 +15,7 @@ interface Props { } class NavigatorComponent extends React.Component { - public override componentDidUpdate(prevProps: Props): void { + public override componentDidUpdate(): void { if (this.props.route) { const routeName = this.props.route.routeName; const screenInfo = this.props.screens[routeName]; @@ -26,12 +25,6 @@ class NavigatorComponent extends React.Component { windowTitle.push(screenInfo.title()); } this.updateWindowTitle(windowTitle.join(' - ')); - - if (prevProps.route.routeName && routeName !== prevProps.route.routeName) { - if (bridge().hasActiveWindow() && bridge().activeWindow() !== bridge().mainWindow()) { - focus('Navigator', bridge().mainWindow()); - } - } } } diff --git a/packages/lib/BaseApplication.ts b/packages/lib/BaseApplication.ts index 5f22f3f094f..527453caabe 100644 --- a/packages/lib/BaseApplication.ts +++ b/packages/lib/BaseApplication.ts @@ -64,6 +64,7 @@ import { join } from 'path'; import processStartFlags from './utils/processStartFlags'; import { setupAutoDeletion } from './services/trash/permanentlyDeleteOldItems'; import determineProfileAndBaseDir from './determineBaseAppDirs'; +import NavService from './services/NavService'; const appLogger: LoggerWrapper = Logger.create('App'); @@ -614,6 +615,7 @@ export default class BaseApplication { BaseModel.dispatch = this.store().dispatch; BaseSyncTarget.dispatch = this.store().dispatch; + NavService.dispatch = this.store().dispatch; DecryptionWorker.instance().dispatch = this.store().dispatch; ResourceFetcher.instance().dispatch = this.store().dispatch; ShareService.instance().initialize(this.store(), EncryptionService.instance()); From e05528915457561d2ef3a10716485af827a84006 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 31 Oct 2024 17:53:36 -0700 Subject: [PATCH 58/97] Fix note viewer in secondary windows --- .../app-desktop/gui/NoteEditor/NoteEditor.tsx | 16 ++++++++++++---- .../gui/NoteEditor/commands/showRevisions.ts | 3 +++ packages/lib/services/CommandService.ts | 14 ++++++++++++-- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx b/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx index a7d541a1d00..59f9c0a580f 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx @@ -356,12 +356,19 @@ function NoteEditorContent(props: NoteEditorProps) { useEffect(() => { const dependencies = { setShowRevisions, + isInFocusedDocument: () => { + return containerRef.current?.ownerDocument?.hasFocus(); + }, }; - CommandService.instance().componentRegisterCommands(dependencies, commands); + const registeredCommands = CommandService.instance().componentRegisterCommands( + dependencies, + commands, + true, + ); return () => { - CommandService.instance().componentUnregisterCommands(commands); + registeredCommands.deregister(); }; }, [setShowRevisions]); @@ -382,7 +389,7 @@ function NoteEditorContent(props: NoteEditorProps) { opacity: 0.1, ...rootStyle, }; - return
    ; + return
    ; } function renderTagButton() { @@ -480,10 +487,11 @@ function NoteEditorContent(props: NoteEditorProps) { padding: theme.margin, verticalAlign: 'top', boxSizing: 'border-box', + flex: 1, }; return ( -
    +
    ); diff --git a/packages/app-desktop/gui/NoteEditor/commands/showRevisions.ts b/packages/app-desktop/gui/NoteEditor/commands/showRevisions.ts index 7eea8396026..1f0c40fd443 100644 --- a/packages/app-desktop/gui/NoteEditor/commands/showRevisions.ts +++ b/packages/app-desktop/gui/NoteEditor/commands/showRevisions.ts @@ -10,5 +10,8 @@ export const runtime = (comp: any): CommandRuntime => { execute: async () => { comp.setShowRevisions(true); }, + getPriority: () => { + return comp.isInFocusedDocument() ? 1 : 0; + }, }; }; diff --git a/packages/lib/services/CommandService.ts b/packages/lib/services/CommandService.ts index 801f2e07e82..8f7506c618b 100644 --- a/packages/lib/services/CommandService.ts +++ b/packages/lib/services/CommandService.ts @@ -257,10 +257,20 @@ export default class CommandService extends BaseService { } } - public componentRegisterCommands(component: ComponentType, commands: ComponentCommandSpec[]) { + public componentRegisterCommands(component: ComponentType, commands: ComponentCommandSpec[], allowMultiple?: boolean) { + const runtimeHandles: RegisteredRuntime[] = []; for (const command of commands) { - CommandService.instance().registerRuntime(command.declaration.name, command.runtime(component)); + runtimeHandles.push( + CommandService.instance().registerRuntime(command.declaration.name, command.runtime(component), allowMultiple), + ); } + return { + deregister: () => { + for (const handle of runtimeHandles) { + handle.deregister(); + } + }, + }; } // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied From 7bc9f7327cd03401565c881b40a9f0352a0d7e1a Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 31 Oct 2024 18:02:28 -0700 Subject: [PATCH 59/97] Make "focus sidebar" and "focus note list" commands show the main window --- packages/app-desktop/bridge.ts | 10 ++++++---- .../gui/NoteList/commands/focusElementNoteList.ts | 5 +++++ .../gui/Sidebar/commands/focusElementSideBar.ts | 3 +++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/app-desktop/bridge.ts b/packages/app-desktop/bridge.ts index a1339de9b41..405e8d26471 100644 --- a/packages/app-desktop/bridge.ts +++ b/packages/app-desktop/bridge.ts @@ -267,14 +267,16 @@ export class Bridge { return this.electronWrapper_.activeWindow(); } - public hasActiveWindow() { - return !!BrowserWindow.getFocusedWindow(); - } - public windowById(id: string) { return this.electronWrapper_.windowById(id); } + public switchToMainWindow() { + if (this.activeWindow() !== this.mainWindow()) { + this.mainWindow().show(); + } + } + public showItemInFolder(fullPath: string) { return require('electron').shell.showItemInFolder(toSystemSlashes(fullPath)); } diff --git a/packages/app-desktop/gui/NoteList/commands/focusElementNoteList.ts b/packages/app-desktop/gui/NoteList/commands/focusElementNoteList.ts index dbedcf93b52..07871933819 100644 --- a/packages/app-desktop/gui/NoteList/commands/focusElementNoteList.ts +++ b/packages/app-desktop/gui/NoteList/commands/focusElementNoteList.ts @@ -2,6 +2,7 @@ import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/ import { _ } from '@joplin/lib/locale'; import { stateUtils } from '@joplin/lib/reducer'; import { FocusNote } from '../utils/useFocusNote'; +import bridge from '../../../services/bridge'; export const declaration: CommandDeclaration = { name: 'focusElementNoteList', @@ -14,6 +15,10 @@ export const runtime = (focusNote: FocusNote): CommandRuntime => { execute: async (context: CommandContext, noteId: string = null) => { noteId = noteId || stateUtils.selectedNoteId(context.state); focusNote(noteId); + + // The sidebar is only present in the main window. If a different window + // is active, the main window needs to be shown. + bridge().switchToMainWindow(); }, enabledCondition: 'noteListHasNotes', }; diff --git a/packages/app-desktop/gui/Sidebar/commands/focusElementSideBar.ts b/packages/app-desktop/gui/Sidebar/commands/focusElementSideBar.ts index a8a6ef168a4..b7c529f641b 100644 --- a/packages/app-desktop/gui/Sidebar/commands/focusElementSideBar.ts +++ b/packages/app-desktop/gui/Sidebar/commands/focusElementSideBar.ts @@ -3,6 +3,7 @@ import { _ } from '@joplin/lib/locale'; import layoutItemProp from '../../ResizableLayout/utils/layoutItemProp'; import { AppState } from '../../../app.reducer'; import { SidebarCommandRuntimeProps } from '../types'; +import bridge from '../../../services/bridge'; export const declaration: CommandDeclaration = { name: 'focusElementSideBar', @@ -17,6 +18,8 @@ export const runtime = (props: SidebarCommandRuntimeProps): CommandRuntime => { if (sidebarVisible) { props.focusSidebar(); + // The sidebar is only present in the main window: + bridge().switchToMainWindow(); } }, From 63e737b6e524ff88f294cc06ad038e8dc7282c9e Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 31 Oct 2024 21:20:43 -0700 Subject: [PATCH 60/97] Fixing tests: Use the same menu for all windows --- packages/app-desktop/gui/MenuBar.tsx | 29 +++++++------------ .../util/activateMainMenuItem.ts | 2 +- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/packages/app-desktop/gui/MenuBar.tsx b/packages/app-desktop/gui/MenuBar.tsx index 1a6bd1297a1..a4b390c8cb6 100644 --- a/packages/app-desktop/gui/MenuBar.tsx +++ b/packages/app-desktop/gui/MenuBar.tsx @@ -178,8 +178,8 @@ interface Props { pluginSettings: PluginSettings; noteListRendererIds: string[]; noteListRendererId: string; - isWindowFocused: boolean; windowId: string; + secondaryWindowFocused: boolean; showMenuBar: boolean; } @@ -1108,15 +1108,11 @@ function MenuBar(props: Props): any { const menu = useMenu(props); useEffect(() => { - if (shim.isMac()) { - // On MacOS, window.setMenu does nothing and the app menu must be set instead - if (props.isWindowFocused) { - Menu.setApplicationMenu(menu); - } - } else { - bridge().windowById(props.windowId)?.setMenu(menu); - } - }, [menu, props.windowId, props.isWindowFocused]); + // Currently, this sets the menu for all windows. Although it's possible to set the menu + // for individual windows with BrowserWindow.setMenu, it causes issues with updating the + // state of existing menu items (and doesn't work with MacOS/Playwright). + Menu.setApplicationMenu(menu); + }, [menu]); useEffect(() => { applyMenuBarVisibility(props.windowId, props.showMenuBar); @@ -1125,22 +1121,19 @@ function MenuBar(props: Props): any { return null; } -interface OwnProps { - windowId: string; -} -const mapStateToProps = (state: AppState, { windowId }: OwnProps) => { +const mapStateToProps = (state: AppState): Partial => { const whenClauseContext = stateToWhenClauseContext(state); - const isSecondaryWindow = windowId !== defaultWindowId; + const secondaryWindowFocused = state.windowId !== defaultWindowId; return { - windowId: windowId, + windowId: state.windowId, menuItemProps: menuUtils.commandsToMenuItemProps(commandNames.concat(getPluginCommandNames(state.pluginService.plugins)), whenClauseContext), locale: state.settings.locale, // Secondary windows can only show the main screen - mainScreenVisible: state.route.routeName === 'Main' || isSecondaryWindow, - isWindowFocused: windowId === state.windowId, + mainScreenVisible: state.route.routeName === 'Main' || secondaryWindowFocused, + selectedFolderId: state.selectedFolderId, layoutButtonSequence: state.settings.layoutButtonSequence, ['notes.sortOrder.field']: state.settings['notes.sortOrder.field'], diff --git a/packages/app-desktop/integration-tests/util/activateMainMenuItem.ts b/packages/app-desktop/integration-tests/util/activateMainMenuItem.ts index 56388863244..cd5d249b791 100644 --- a/packages/app-desktop/integration-tests/util/activateMainMenuItem.ts +++ b/packages/app-desktop/integration-tests/util/activateMainMenuItem.ts @@ -41,7 +41,7 @@ const activateMainMenuItem = async ( }; const appMenu = Menu.getApplicationMenu(); - return activateItemInSubmenu(appMenu.items, ''); + return !!appMenu && activateItemInSubmenu(appMenu.items, ''); }, [targetItemLabel, parentMenuLabel]); }, { message: `should find and activate menu item with label ${JSON.stringify(targetItemLabel)}`, From 294018a12970656d5676d3fe2c90db3bb1733d10 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 31 Oct 2024 21:30:09 -0700 Subject: [PATCH 61/97] When closing the last user-visible window, quit the app if the tray icon is hidden --- packages/app-desktop/ElectronAppWrapper.ts | 59 +++++++++++++++------- 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/packages/app-desktop/ElectronAppWrapper.ts b/packages/app-desktop/ElectronAppWrapper.ts index 57add4507f3..143f2664776 100644 --- a/packages/app-desktop/ElectronAppWrapper.ts +++ b/packages/app-desktop/ElectronAppWrapper.ts @@ -29,22 +29,30 @@ interface PluginWindows { [key: string]: any; } -export default class ElectronAppWrapper { +type SecondaryWindowId = string; +interface SecondaryWindowData { + electronId: number; +} +export default class ElectronAppWrapper { private logger_: Logger = null; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied private electronApp_: any; private env_: string; private isDebugMode_: boolean; private profilePath_: string; + private win_: BrowserWindow = null; + private mainWindowHidden_ = true; + private pluginWindows_: PluginWindows = {}; + private secondaryWindows_: Map = new Map(); + private willQuitApp_ = false; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied private tray_: any = null; private buildDir_: string = null; private rendererProcessQuitReply_: RendererProcessQuitReply = null; - private pluginWindows_: PluginWindows = {}; - private joplinWindowIdToElectronId_: Map = new Map(); + private initialCallbackUrl_: string = null; private updaterService_: AutoUpdaterService = null; private customProtocolHandler_: CustomProtocolHandler = null; @@ -84,9 +92,9 @@ export default class ElectronAppWrapper { return this.mainWindow(); } - const electronId = this.joplinWindowIdToElectronId_.get(joplinId); - if (electronId !== undefined) { - return BrowserWindow.fromId(electronId); + const windowData = this.secondaryWindows_.get(joplinId); + if (windowData !== undefined) { + return BrowserWindow.fromId(windowData.electronId); } return null; } @@ -229,6 +237,15 @@ export default class ElectronAppWrapper { } }); + this.mainWindowHidden_ = !windowOptions.show; + this.win_.on('hide', () => { + this.mainWindowHidden_ = true; + }); + + this.win_.on('show', () => { + this.mainWindowHidden_ = false; + }); + void this.win_.loadURL(url.format({ pathname: path.join(__dirname, 'index.html'), protocol: 'file:', @@ -305,7 +322,8 @@ export default class ElectronAppWrapper { this.hide(); } } else { - if (this.trayShown() && !this.willQuitApp_) { + const hasBackgroundWindows = this.secondaryWindows_.size > 0; + if ((hasBackgroundWindows || this.trayShown()) && !this.willQuitApp_) { event.preventDefault(); this.win_.hide(); } else { @@ -335,6 +353,23 @@ export default class ElectronAppWrapper { } }); + ipcMain.on('secondary-window-added', (event, windowId: string) => { + const window = BrowserWindow.fromWebContents(event.sender); + const electronWindowId = window?.id; + this.secondaryWindows_.set(windowId, { electronId: electronWindowId }); + + window.once('close', () => { + this.secondaryWindows_.delete(windowId); + + const allSecondaryWindowsClosed = this.secondaryWindows_.size === 0; + const mainWindowVisuallyClosed = this.mainWindowHidden_; + if (allSecondaryWindowsClosed && mainWindowVisuallyClosed && !this.trayShown()) { + // Gracefully quit the app if the user has closed all windows + this.win_.close(); + } + }); + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied ipcMain.on('asynchronous-message', (_event: any, message: string, args: any) => { if (message === 'appCloseReply') { @@ -381,16 +416,6 @@ export default class ElectronAppWrapper { void this.updaterService_.checkForUpdates(true); }); - ipcMain.on('secondary-window-added', (event, windowId: string) => { - const window = BrowserWindow.fromWebContents(event.sender); - const electronWindowId = window?.id; - this.joplinWindowIdToElectronId_.set(windowId, electronWindowId); - - window.once('close', () => { - this.joplinWindowIdToElectronId_.delete(windowId); - }); - }); - // Let us register listeners on the window, so we can update the state // automatically (the listeners will be removed when the window is closed) // and restore the maximized or full screen state From 406cb83cd78a5ea934b69c58fa1ca99c57e87ca8 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 31 Oct 2024 21:30:37 -0700 Subject: [PATCH 62/97] Fix tsc: Remove unused prop --- packages/app-desktop/gui/NoteEditor/EditorWindow.tsx | 2 -- packages/app-desktop/gui/Root.tsx | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/app-desktop/gui/NoteEditor/EditorWindow.tsx b/packages/app-desktop/gui/NoteEditor/EditorWindow.tsx index 380a643d0ef..8ee58aa087d 100644 --- a/packages/app-desktop/gui/NoteEditor/EditorWindow.tsx +++ b/packages/app-desktop/gui/NoteEditor/EditorWindow.tsx @@ -13,7 +13,6 @@ const { StyleSheetManager } = require('styled-components'); import createCache from '@emotion/cache'; import { CacheProvider } from '@emotion/react'; import { stateUtils } from '@joplin/lib/reducer'; -import MenuBar from '../MenuBar'; interface Props { dispatch: Dispatch; @@ -72,7 +71,6 @@ const SecondaryWindow: React.FC = props => { title={windowTitle} > - {editor} diff --git a/packages/app-desktop/gui/Root.tsx b/packages/app-desktop/gui/Root.tsx index 0e5185dee84..34a09b3f519 100644 --- a/packages/app-desktop/gui/Root.tsx +++ b/packages/app-desktop/gui/Root.tsx @@ -257,7 +257,7 @@ class RootComponent extends React.Component { - + From 888a78fa418a31be2027a3ed874f747cdd74654e Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 31 Oct 2024 21:43:20 -0700 Subject: [PATCH 63/97] Fix menubar flashes on startup --- packages/app-desktop/gui/MenuBar.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/app-desktop/gui/MenuBar.tsx b/packages/app-desktop/gui/MenuBar.tsx index a4b390c8cb6..2b3e031e7fa 100644 --- a/packages/app-desktop/gui/MenuBar.tsx +++ b/packages/app-desktop/gui/MenuBar.tsx @@ -1111,7 +1111,9 @@ function MenuBar(props: Props): any { // Currently, this sets the menu for all windows. Although it's possible to set the menu // for individual windows with BrowserWindow.setMenu, it causes issues with updating the // state of existing menu items (and doesn't work with MacOS/Playwright). - Menu.setApplicationMenu(menu); + if (menu) { + Menu.setApplicationMenu(menu); + } }, [menu]); useEffect(() => { From 536d9120fbbbc24e5e107173d0e30d072cb2217b Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 31 Oct 2024 21:46:53 -0700 Subject: [PATCH 64/97] Fix print command --- .../app-desktop/gui/WindowCommandHandler/commands/print.ts | 6 +++--- .../gui/WindowCommandHandler/utils/usePrintToCallback.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/print.ts b/packages/app-desktop/gui/WindowCommandHandler/commands/print.ts index 3940c1cb64f..06204f00fee 100644 --- a/packages/app-desktop/gui/WindowCommandHandler/commands/print.ts +++ b/packages/app-desktop/gui/WindowCommandHandler/commands/print.ts @@ -1,5 +1,6 @@ import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService'; import { _ } from '@joplin/lib/locale'; +import { WindowControl } from '../utils/useWindowControl'; const bridge = require('@electron/remote').require('./bridge').default; export const declaration: CommandDeclaration = { @@ -8,15 +9,14 @@ export const declaration: CommandDeclaration = { iconName: 'fa-file', }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied -export const runtime = (comp: any): CommandRuntime => { +export const runtime = (comp: WindowControl): CommandRuntime => { return { execute: async (context: CommandContext, noteIds: string[] = null) => { noteIds = noteIds || context.state.selectedNoteIds; try { if (noteIds.length !== 1) throw new Error(_('Only one note can be printed at a time.')); - await comp.printTo_('printer', { noteId: noteIds[0] }); + await comp.printTo('printer', { noteId: noteIds[0] }); } catch (error) { bridge().showErrorMessageBox(error.message); } diff --git a/packages/app-desktop/gui/WindowCommandHandler/utils/usePrintToCallback.ts b/packages/app-desktop/gui/WindowCommandHandler/utils/usePrintToCallback.ts index b8beec882cd..ff1a2cd63b0 100644 --- a/packages/app-desktop/gui/WindowCommandHandler/utils/usePrintToCallback.ts +++ b/packages/app-desktop/gui/WindowCommandHandler/utils/usePrintToCallback.ts @@ -15,7 +15,7 @@ interface Props { } interface PrintOptions { - path: string; + path?: string; noteId: string; } @@ -37,7 +37,7 @@ const usePrintToCallback = (props: Props): PrintCallback => { // Concurrent print calls are disallowed to avoid incorrect settings being restored upon completion if (isPrinting) { // eslint-disable-next-line no-console -- Old code from before rule was applied - console.info(`Printing ${options.path} to ${target} disallowed, already printing.`); + console.info(`Printing ${options.path ?? options.noteId} to ${target} disallowed, already printing.`); return; } From 4c65e5eefeb6c70525c74388eb787523c186320d Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 31 Oct 2024 21:59:21 -0700 Subject: [PATCH 65/97] Fix certain dialogs are only shown in the main window --- .eslintignore | 1 + .gitignore | 1 + packages/app-desktop/app.reducer.ts | 4 +- packages/app-desktop/gui/Root.tsx | 64 ------------------- .../WindowCommandHandler.tsx | 22 ++++++- .../WindowCommandHandler/utils/appDialogs.tsx | 49 ++++++++++++++ 6 files changed, 74 insertions(+), 67 deletions(-) create mode 100644 packages/app-desktop/gui/WindowCommandHandler/utils/appDialogs.tsx diff --git a/.eslintignore b/.eslintignore index 290531de919..4718836c894 100644 --- a/.eslintignore +++ b/.eslintignore @@ -460,6 +460,7 @@ packages/app-desktop/gui/WindowCommandHandler/commands/togglePerFolderSortOrder. packages/app-desktop/gui/WindowCommandHandler/commands/toggleSideBar.js packages/app-desktop/gui/WindowCommandHandler/commands/toggleVisiblePanes.js packages/app-desktop/gui/WindowCommandHandler/types.js +packages/app-desktop/gui/WindowCommandHandler/utils/appDialogs.js packages/app-desktop/gui/WindowCommandHandler/utils/usePrintToCallback.js packages/app-desktop/gui/WindowCommandHandler/utils/useWindowControl.js packages/app-desktop/gui/dialogs.js diff --git a/.gitignore b/.gitignore index c548051646b..e0de3d430fa 100644 --- a/.gitignore +++ b/.gitignore @@ -437,6 +437,7 @@ packages/app-desktop/gui/WindowCommandHandler/commands/togglePerFolderSortOrder. packages/app-desktop/gui/WindowCommandHandler/commands/toggleSideBar.js packages/app-desktop/gui/WindowCommandHandler/commands/toggleVisiblePanes.js packages/app-desktop/gui/WindowCommandHandler/types.js +packages/app-desktop/gui/WindowCommandHandler/utils/appDialogs.js packages/app-desktop/gui/WindowCommandHandler/utils/usePrintToCallback.js packages/app-desktop/gui/WindowCommandHandler/utils/useWindowControl.js packages/app-desktop/gui/dialogs.js diff --git a/packages/app-desktop/app.reducer.ts b/packages/app-desktop/app.reducer.ts index 77a28011fc5..4d49a47351d 100644 --- a/packages/app-desktop/app.reducer.ts +++ b/packages/app-desktop/app.reducer.ts @@ -38,6 +38,7 @@ export interface AppWindowState extends WindowState { noteVisiblePanes: string[]; editorCodeView: boolean; visibleDialogs: VisibleDialogs; + dialogs: AppStateDialog[]; devToolsVisible: boolean; } @@ -60,7 +61,6 @@ export interface AppState extends State, AppWindowState { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied watchedResources: any; mainLayout: LayoutItem; - dialogs: AppStateDialog[]; isResettingLayout: boolean; } @@ -68,6 +68,7 @@ export const createAppDefaultWindowState = (): AppWindowState => { return { ...defaultWindowState, visibleDialogs: {}, + dialogs: [], noteVisiblePanes: ['editor', 'viewer'], editorCodeView: true, devToolsVisible: false, @@ -93,7 +94,6 @@ export function createAppDefaultState(windowContentSize: any, resourceEditWatche layoutMoveMode: false, mainLayout: null, startupPluginsLoaded: false, - dialogs: [], isResettingLayout: false, modalOverlayMessage: null, ...resourceEditWatcherDefaultState, diff --git a/packages/app-desktop/gui/Root.tsx b/packages/app-desktop/gui/Root.tsx index 34a09b3f519..212dd27985a 100644 --- a/packages/app-desktop/gui/Root.tsx +++ b/packages/app-desktop/gui/Root.tsx @@ -19,10 +19,6 @@ import ClipperServer from '@joplin/lib/ClipperServer'; import DialogTitle from './DialogTitle'; import DialogButtonRow, { ButtonSpec, ClickEvent, ClickEventHandler } from './DialogButtonRow'; import Dialog from './Dialog'; -import SyncWizardDialog from './SyncWizard/Dialog'; -import MasterPasswordDialog from './MasterPasswordDialog/Dialog'; -import EditFolderDialog from './EditFolderDialog/Dialog'; -import PdfViewer from './PdfViewer'; import StyleSheetContainer from './StyleSheets/StyleSheetContainer'; import ImportScreen from './ImportScreen'; const { ResourceScreen } = require('./ResourceScreen.js'); @@ -54,46 +50,6 @@ interface ModalDialogProps { onClick: ClickEventHandler; } -interface RegisteredDialogProps { - themeId: number; - key: string; - // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied - dispatch: Function; -} - -interface RegisteredDialog { - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - render: (props: RegisteredDialogProps, customProps: any)=> any; -} - -const registeredDialogs: Record = { - syncWizard: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - render: (props: RegisteredDialogProps, customProps: any) => { - return ; - }, - }, - - masterPassword: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - render: (props: RegisteredDialogProps, customProps: any) => { - return ; - }, - }, - - editFolder: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - render: (props: RegisteredDialogProps, customProps: any) => { - return ; - }, - }, - pdfViewer: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - render: (props: RegisteredDialogProps, customProps: any) => { - return ; - }, - }, -}; const GlobalStyle = createGlobalStyle` * { @@ -205,25 +161,6 @@ class RootComponent extends React.Component { }; } - private renderDialogs() { - const props: Props = this.props; - - if (!props.dialogs.length) return null; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - const output: any[] = []; - for (const dialog of props.dialogs) { - const md = registeredDialogs[dialog.name]; - if (!md) throw new Error(`Unknown dialog: ${dialog.name}`); - output.push(md.render({ - key: dialog.name, - themeId: props.themeId, - dispatch: props.dispatch, - }, dialog.props)); - } - return output; - } - private renderSecondaryWindows() { return this.props.secondaryWindowStates.map((windowState: WindowState) => { return { {this.renderSecondaryWindows()} {this.renderModalMessage(this.modalDialogProps())} - {this.renderDialogs()} ); diff --git a/packages/app-desktop/gui/WindowCommandHandler/WindowCommandHandler.tsx b/packages/app-desktop/gui/WindowCommandHandler/WindowCommandHandler.tsx index 4659318259f..fa45db3fab3 100644 --- a/packages/app-desktop/gui/WindowCommandHandler/WindowCommandHandler.tsx +++ b/packages/app-desktop/gui/WindowCommandHandler/WindowCommandHandler.tsx @@ -11,7 +11,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { DialogState } from './types'; import usePrintToCallback from './utils/usePrintToCallback'; import { connect } from 'react-redux'; -import { AppState, VisibleDialogs } from '../../app.reducer'; +import { AppState, AppStateDialog, VisibleDialogs } from '../../app.reducer'; import useWindowControl, { WindowControl } from './utils/useWindowControl'; import commands from './commands'; import CommandService, { CommandRuntime, ComponentCommandSpec } from '@joplin/lib/services/CommandService'; @@ -19,6 +19,7 @@ import { Dispatch } from 'redux'; import ModalMessageOverlay from './ModalMessageOverlay'; import useNowEffect from '@joplin/lib/hooks/useNowEffect'; import { stateUtils } from '@joplin/lib/reducer'; +import appDialogs from './utils/appDialogs'; const PluginManager = require('@joplin/lib/services/PluginManager'); @@ -28,6 +29,7 @@ interface Props { plugins: PluginStates; pluginHtmlContents: PluginHtmlContents; currentWindowDialogs: VisibleDialogs; + appDialogs: AppStateDialog[]; pluginsLegacy: unknown; modalMessage: string|null; @@ -165,6 +167,22 @@ const WindowCommandHandler: React.FC = props => { ); }; + const renderAppDialogs = () => { + if (!props.appDialogs.length) return null; + + const output: React.ReactNode[] = []; + for (const dialog of props.appDialogs) { + const md = appDialogs[dialog.name]; + if (!md) throw new Error(`Unknown dialog: ${dialog.name}`); + output.push(md.render({ + key: dialog.name, + themeId: props.themeId, + dispatch: props.dispatch, + }, dialog.props)); + } + return output; + }; + const promptOnClose = useCallback((answer: unknown, buttonType: unknown) => { dialogState.promptOptions.onClose(answer, buttonType); }, [dialogState.promptOptions]); @@ -216,6 +234,7 @@ const WindowCommandHandler: React.FC = props => { onClose={onDialogHideCallbacks.shareFolderDialogOptions} /> )} + {renderAppDialogs()} { themeId: state.settings.theme, plugins: state.pluginService.plugins, currentWindowDialogs: windowState.visibleDialogs, + appDialogs: windowState.dialogs, pluginHtmlContents: state.pluginService.pluginHtmlContents, customCss: state.customViewerCss, editorNoteStatuses: state.editorNoteStatuses, diff --git a/packages/app-desktop/gui/WindowCommandHandler/utils/appDialogs.tsx b/packages/app-desktop/gui/WindowCommandHandler/utils/appDialogs.tsx new file mode 100644 index 00000000000..07a17fcdf45 --- /dev/null +++ b/packages/app-desktop/gui/WindowCommandHandler/utils/appDialogs.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; + +import SyncWizardDialog from '../../SyncWizard/Dialog'; +import MasterPasswordDialog from '../../MasterPasswordDialog/Dialog'; +import EditFolderDialog from '../../EditFolderDialog/Dialog'; +import PdfViewer from '../../PdfViewer'; + +interface RegisteredDialogProps { + themeId: number; + key: string; + // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied + dispatch: Function; +} + +interface RegisteredDialog { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + render: (props: RegisteredDialogProps, customProps: any)=> any; +} + +const appDialogs: Record = { + syncWizard: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + render: (props: RegisteredDialogProps, customProps: any) => { + return ; + }, + }, + + masterPassword: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + render: (props: RegisteredDialogProps, customProps: any) => { + return ; + }, + }, + + editFolder: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + render: (props: RegisteredDialogProps, customProps: any) => { + return ; + }, + }, + pdfViewer: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied + render: (props: RegisteredDialogProps, customProps: any) => { + return ; + }, + }, +}; + +export default appDialogs; From 7bbd59c4a1c3f6f8f70029878e8c0d7fe1f18cff Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 31 Oct 2024 22:18:15 -0700 Subject: [PATCH 66/97] Switch focus to the main window when it navigates For example, if a sync causes the "sign in to Dropbox" screen to be shown in the main window and a different window has focus, this commit causes focus to move to the main window. --- packages/app-desktop/bridge.ts | 14 +++-- packages/app-desktop/gui/Navigator.tsx | 72 ++++++++++++++------------ 2 files changed, 50 insertions(+), 36 deletions(-) diff --git a/packages/app-desktop/bridge.ts b/packages/app-desktop/bridge.ts index 405e8d26471..1ca6168acbc 100644 --- a/packages/app-desktop/bridge.ts +++ b/packages/app-desktop/bridge.ts @@ -14,6 +14,7 @@ import { extname, normalize } from 'path'; import isSafeToOpen from './utils/isSafeToOpen'; import { closeSync, openSync, readSync, statSync } from 'fs'; import { KB } from '@joplin/utils/bytes'; +import { defaultWindowId } from '@joplin/lib/reducer'; interface LastSelectedPath { file: string; @@ -271,12 +272,19 @@ export class Bridge { return this.electronWrapper_.windowById(id); } - public switchToMainWindow() { - if (this.activeWindow() !== this.mainWindow()) { - this.mainWindow().show(); + // Switches to the window with the given ID, but only if that window was not the + // last focused window + public switchToWindow(windowId: string) { + const targetWindow = this.windowById(windowId); + if (this.activeWindow() !== this.windowById(windowId)) { + targetWindow.show(); } } + public switchToMainWindow() { + this.switchToWindow(defaultWindowId); + } + public showItemInFolder(fullPath: string) { return require('electron').shell.showItemInFolder(toSystemSlashes(fullPath)); } diff --git a/packages/app-desktop/gui/Navigator.tsx b/packages/app-desktop/gui/Navigator.tsx index 9a757721c0f..ae04510e2e7 100644 --- a/packages/app-desktop/gui/Navigator.tsx +++ b/packages/app-desktop/gui/Navigator.tsx @@ -1,12 +1,13 @@ import * as React from 'react'; const { connect } = require('react-redux'); import Setting from '@joplin/lib/models/Setting'; -import { AppState } from '../app.reducer'; +import { AppState, AppStateRoute } from '../app.reducer'; import bridge from '../services/bridge'; +import { useContext, useEffect, useRef } from 'react'; +import { WindowIdContext } from './NewWindowOrIFrame'; interface Props { - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - route: any; + route: AppStateRoute; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied screens: Record; @@ -14,48 +15,53 @@ interface Props { className?: string; } -class NavigatorComponent extends React.Component { - public override componentDidUpdate(): void { - if (this.props.route) { - const routeName = this.props.route.routeName; - const screenInfo = this.props.screens[routeName]; +const NavigatorComponent: React.FC = props => { + const windowId = useContext(WindowIdContext); + + const route = props.route; + const screenInfo = props.screens[route?.routeName]; + + const screensRef = useRef(props.screens); + screensRef.current = props.screens; + + const prevRoute = useRef(null); + useEffect(() => { + const routeName = route?.routeName; + if (route) { const devMarker = Setting.value('env') === 'dev' ? ` (DEV - ${Setting.value('profileDir')})` : ''; const windowTitle = [`Joplin${devMarker}`]; if (screenInfo.title) { windowTitle.push(screenInfo.title()); } - this.updateWindowTitle(windowTitle.join(' - ')); + bridge().windowById(windowId)?.setTitle(windowTitle.join(' - ')); } - } - public updateWindowTitle(title: string) { - try { - if (bridge().mainWindow()) bridge().mainWindow().setTitle(title); - } catch (error) { - console.warn('updateWindowTitle', error); + // When a navigation happens in an unfocused window, show the window to the user. + // This might happen if, for example, a secondary window triggers a navigation in + // the main window. + if (routeName && routeName !== prevRoute.current?.routeName) { + bridge().switchToWindow(windowId); } - } - public render() { - if (!this.props.route) throw new Error('Route must not be null'); + prevRoute.current = route; + }, [route, screenInfo, windowId]); - const route = this.props.route; - const screenProps = route.props ? route.props : {}; - const screenInfo = this.props.screens[route.routeName]; - const Screen = screenInfo.screen; + if (!route) throw new Error('Route must not be null'); - const screenStyle = { - width: this.props.style.width, - height: this.props.style.height, - }; + const screenProps = route.props ? route.props : {}; + const Screen = screenInfo.screen; - return ( -
    - -
    - ); - } -} + const screenStyle = { + width: props.style.width, + height: props.style.height, + }; + + return ( +
    + +
    + ); +}; const Navigator = connect((state: AppState) => { return { From cbaac1c1a3b8870e0963df6669a6d32939bcffa9 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 1 Nov 2024 10:16:21 -0700 Subject: [PATCH 67/97] Only allow one copy of each plugin dialog to be open at a time --- packages/app-desktop/app.reducer.ts | 11 +++++++++++ packages/lib/services/plugins/WebviewController.ts | 6 ++++++ .../lib/services/plugins/api/JoplinViewsDialogs.ts | 4 +++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/app-desktop/app.reducer.ts b/packages/app-desktop/app.reducer.ts index 4d49a47351d..a68c2c05cdb 100644 --- a/packages/app-desktop/app.reducer.ts +++ b/packages/app-desktop/app.reducer.ts @@ -100,6 +100,15 @@ export function createAppDefaultState(windowContentSize: any, resourceEditWatche }; } +const hideBackgroundDialogsMatching = produce((state: AppState, id: string) => { + for (const windowId of Object.keys(state.backgroundWindows)) { + const win = state.backgroundWindows[windowId]; + if (id in win.visibleDialogs) { + delete win.visibleDialogs[id]; + } + } +}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied export default function(state: AppState, action: any) { let newState = state; @@ -308,12 +317,14 @@ export default function(state: AppState, action: any) { newState = { ...state }; newState.visibleDialogs = { ...newState.visibleDialogs }; newState.visibleDialogs[action.name] = true; + newState = hideBackgroundDialogsMatching(newState, action.name); break; case 'VISIBLE_DIALOGS_REMOVE': newState = { ...state }; newState.visibleDialogs = { ...newState.visibleDialogs }; delete newState.visibleDialogs[action.name]; + newState = hideBackgroundDialogsMatching(newState, action.name); break; case 'FOCUS_SET': diff --git a/packages/lib/services/plugins/WebviewController.ts b/packages/lib/services/plugins/WebviewController.ts index 5bc25c96c6e..5b94697c514 100644 --- a/packages/lib/services/plugins/WebviewController.ts +++ b/packages/lib/services/plugins/WebviewController.ts @@ -190,6 +190,11 @@ export default class WebviewController extends ViewController { // --------------------------------------------- public async open(): Promise { + if (this.closeResponse_) { + this.closeResponse_.resolve(null); + this.closeResponse_ = null; + } + this.store.dispatch({ type: 'VISIBLE_DIALOGS_ADD', name: this.handle, @@ -215,6 +220,7 @@ export default class WebviewController extends ViewController { public closeWithResponse(result: DialogResult) { this.close(); this.closeResponse_.resolve(result); + this.closeResponse_ = null; } public get buttons(): ButtonSpec[] { diff --git a/packages/lib/services/plugins/api/JoplinViewsDialogs.ts b/packages/lib/services/plugins/api/JoplinViewsDialogs.ts index 371cd956048..650a2e627e4 100644 --- a/packages/lib/services/plugins/api/JoplinViewsDialogs.ts +++ b/packages/lib/services/plugins/api/JoplinViewsDialogs.ts @@ -110,7 +110,9 @@ export default class JoplinViewsDialogs { } /** - * Opens the dialog + * Opens the dialog. + * + * On desktop, this closes any copies of the dialog open in different windows. */ public async open(handle: ViewHandle): Promise { return this.controller(handle).open(); From f04b00f43ffa3d4eba4b1b58f4185cdf7f0ec851 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 1 Nov 2024 11:32:49 -0700 Subject: [PATCH 68/97] Refactoring: Code cleanup and preparation for per-window smalltalk/notyf --- .eslintignore | 4 + .gitignore | 4 + .../gui/WindowCommandHandler/AppDialogs.tsx | 28 +++ .../WindowCommandHandler/PluginDialogs.tsx | 45 +++++ .../WindowCommandHandler.tsx | 182 ++++++------------ .../utils/useSyncDialogState.ts | 41 ++++ .../utils/useWindowCommands.ts | 49 +++++ .../utils/useWindowControl.ts | 2 +- packages/app-desktop/gui/dialogs.ts | 24 ++- packages/lib/reducer.ts | 7 +- 10 files changed, 252 insertions(+), 134 deletions(-) create mode 100644 packages/app-desktop/gui/WindowCommandHandler/AppDialogs.tsx create mode 100644 packages/app-desktop/gui/WindowCommandHandler/PluginDialogs.tsx create mode 100644 packages/app-desktop/gui/WindowCommandHandler/utils/useSyncDialogState.ts create mode 100644 packages/app-desktop/gui/WindowCommandHandler/utils/useWindowCommands.ts diff --git a/.eslintignore b/.eslintignore index 4718836c894..5d6e3d3333f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -409,7 +409,9 @@ packages/app-desktop/gui/ToolbarButton/ToolbarButton.js packages/app-desktop/gui/ToolbarSpace.js packages/app-desktop/gui/TrashNotification/TrashNotification.js packages/app-desktop/gui/UpdateNotification/UpdateNotification.js +packages/app-desktop/gui/WindowCommandHandler/AppDialogs.js packages/app-desktop/gui/WindowCommandHandler/ModalMessageOverlay.js +packages/app-desktop/gui/WindowCommandHandler/PluginDialogs.js packages/app-desktop/gui/WindowCommandHandler/WindowCommandHandler.js packages/app-desktop/gui/WindowCommandHandler/commands/addProfile.js packages/app-desktop/gui/WindowCommandHandler/commands/commandPalette.js @@ -462,6 +464,8 @@ packages/app-desktop/gui/WindowCommandHandler/commands/toggleVisiblePanes.js packages/app-desktop/gui/WindowCommandHandler/types.js packages/app-desktop/gui/WindowCommandHandler/utils/appDialogs.js packages/app-desktop/gui/WindowCommandHandler/utils/usePrintToCallback.js +packages/app-desktop/gui/WindowCommandHandler/utils/useSyncDialogState.js +packages/app-desktop/gui/WindowCommandHandler/utils/useWindowCommands.js packages/app-desktop/gui/WindowCommandHandler/utils/useWindowControl.js packages/app-desktop/gui/dialogs.js packages/app-desktop/gui/hooks/useDom.js diff --git a/.gitignore b/.gitignore index e0de3d430fa..1eedede4161 100644 --- a/.gitignore +++ b/.gitignore @@ -386,7 +386,9 @@ packages/app-desktop/gui/ToolbarButton/ToolbarButton.js packages/app-desktop/gui/ToolbarSpace.js packages/app-desktop/gui/TrashNotification/TrashNotification.js packages/app-desktop/gui/UpdateNotification/UpdateNotification.js +packages/app-desktop/gui/WindowCommandHandler/AppDialogs.js packages/app-desktop/gui/WindowCommandHandler/ModalMessageOverlay.js +packages/app-desktop/gui/WindowCommandHandler/PluginDialogs.js packages/app-desktop/gui/WindowCommandHandler/WindowCommandHandler.js packages/app-desktop/gui/WindowCommandHandler/commands/addProfile.js packages/app-desktop/gui/WindowCommandHandler/commands/commandPalette.js @@ -439,6 +441,8 @@ packages/app-desktop/gui/WindowCommandHandler/commands/toggleVisiblePanes.js packages/app-desktop/gui/WindowCommandHandler/types.js packages/app-desktop/gui/WindowCommandHandler/utils/appDialogs.js packages/app-desktop/gui/WindowCommandHandler/utils/usePrintToCallback.js +packages/app-desktop/gui/WindowCommandHandler/utils/useSyncDialogState.js +packages/app-desktop/gui/WindowCommandHandler/utils/useWindowCommands.js packages/app-desktop/gui/WindowCommandHandler/utils/useWindowControl.js packages/app-desktop/gui/dialogs.js packages/app-desktop/gui/hooks/useDom.js diff --git a/packages/app-desktop/gui/WindowCommandHandler/AppDialogs.tsx b/packages/app-desktop/gui/WindowCommandHandler/AppDialogs.tsx new file mode 100644 index 00000000000..11226d6883f --- /dev/null +++ b/packages/app-desktop/gui/WindowCommandHandler/AppDialogs.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { AppStateDialog } from '../../app.reducer'; +import appDialogs from './utils/appDialogs'; +import { Dispatch } from 'redux'; + +interface Props { + themeId: number; + dispatch: Dispatch; + appDialogStates: AppStateDialog[]; +} + +const AppDialogs: React.FC = props => { + if (!props.appDialogStates.length) return null; + + const output: React.ReactNode[] = []; + for (const dialog of props.appDialogStates) { + const md = appDialogs[dialog.name]; + if (!md) throw new Error(`Unknown dialog: ${dialog.name}`); + output.push(md.render({ + key: dialog.name, + themeId: props.themeId, + dispatch: props.dispatch, + }, dialog.props)); + } + return <>{output}; +}; + +export default AppDialogs; diff --git a/packages/app-desktop/gui/WindowCommandHandler/PluginDialogs.tsx b/packages/app-desktop/gui/WindowCommandHandler/PluginDialogs.tsx new file mode 100644 index 00000000000..f8e105b9e8b --- /dev/null +++ b/packages/app-desktop/gui/WindowCommandHandler/PluginDialogs.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import UserWebviewDialog from '../../services/plugins/UserWebviewDialog'; +import { PluginHtmlContents, PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer'; +import { ContainerType } from '@joplin/lib/services/plugins/WebviewController'; +import { VisibleDialogs } from '../../app.reducer'; + +interface Props { + themeId: number; + visibleDialogs: VisibleDialogs; + pluginHtmlContents: PluginHtmlContents; + plugins: PluginStates; +} + +const PluginDialogs: React.FC = props => { + const output = []; + const infos = pluginUtils.viewInfosByType(props.plugins, 'webview'); + + for (const info of infos) { + const { plugin, view } = info; + if (view.containerType !== ContainerType.Dialog) continue; + if (!props.visibleDialogs[view.id]) continue; + const html = props.pluginHtmlContents[plugin.id]?.[view.id] ?? ''; + + output.push(); + } + + if (!output.length) return null; + + return ( +
    + {output} +
    + ); +}; + +export default PluginDialogs; diff --git a/packages/app-desktop/gui/WindowCommandHandler/WindowCommandHandler.tsx b/packages/app-desktop/gui/WindowCommandHandler/WindowCommandHandler.tsx index fa45db3fab3..6af867dc325 100644 --- a/packages/app-desktop/gui/WindowCommandHandler/WindowCommandHandler.tsx +++ b/packages/app-desktop/gui/WindowCommandHandler/WindowCommandHandler.tsx @@ -4,22 +4,20 @@ import ShareFolderDialog from '../ShareFolderDialog/ShareFolderDialog'; import NotePropertiesDialog from '../NotePropertiesDialog'; import NoteContentPropertiesDialog from '../NoteContentPropertiesDialog'; import ShareNoteDialog from '../ShareNoteDialog'; -import { PluginHtmlContents, PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer'; -import { ContainerType } from '@joplin/lib/services/plugins/WebviewController'; -import UserWebviewDialog from '../../services/plugins/UserWebviewDialog'; +import { PluginHtmlContents, PluginStates } from '@joplin/lib/services/plugins/reducer'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { DialogState } from './types'; -import usePrintToCallback from './utils/usePrintToCallback'; import { connect } from 'react-redux'; import { AppState, AppStateDialog, VisibleDialogs } from '../../app.reducer'; -import useWindowControl, { WindowControl } from './utils/useWindowControl'; -import commands from './commands'; -import CommandService, { CommandRuntime, ComponentCommandSpec } from '@joplin/lib/services/CommandService'; import { Dispatch } from 'redux'; import ModalMessageOverlay from './ModalMessageOverlay'; -import useNowEffect from '@joplin/lib/hooks/useNowEffect'; -import { stateUtils } from '@joplin/lib/reducer'; -import appDialogs from './utils/appDialogs'; +import { EditorNoteStatuses, stateUtils } from '@joplin/lib/reducer'; +import dialogs from '../dialogs'; +import useDom from '../hooks/useDom'; +import useWindowCommands from './utils/useWindowCommands'; +import PluginDialogs from './PluginDialogs'; +import useSyncDialogState from './utils/useSyncDialogState'; +import AppDialogs from './AppDialogs'; const PluginManager = require('@joplin/lib/services/PluginManager'); @@ -28,13 +26,13 @@ interface Props { themeId: number; plugins: PluginStates; pluginHtmlContents: PluginHtmlContents; - currentWindowDialogs: VisibleDialogs; - appDialogs: AppStateDialog[]; + visibleDialogs: VisibleDialogs; + appDialogStates: AppStateDialog[]; pluginsLegacy: unknown; modalMessage: string|null; customCss: string; - editorNoteStatuses: Record; + editorNoteStatuses: EditorNoteStatuses; } const defaultDialogState: DialogState = { @@ -53,72 +51,44 @@ const defaultDialogState: DialogState = { promptOptions: null, }; -const WindowCommandHandler: React.FC = props => { - const [dialogState, setDialogState] = useState(defaultDialogState); - - const lastDialogStateRef = useRef(dialogState); +// Certain dialog libraries need a reference to the active window: +const useSyncActiveWindow = (containerWindow: Window|null) => { useEffect(() => { - const prevState = lastDialogStateRef.current; - const state = dialogState; - if (state.notePropertiesDialogOptions !== prevState.notePropertiesDialogOptions) { - props.dispatch({ - type: state.notePropertiesDialogOptions && state.notePropertiesDialogOptions.visible ? 'VISIBLE_DIALOGS_ADD' : 'VISIBLE_DIALOGS_REMOVE', - name: 'noteProperties', - }); - } + if (!containerWindow) return () => {}; - if (state.noteContentPropertiesDialogOptions !== prevState.noteContentPropertiesDialogOptions) { - props.dispatch({ - type: state.noteContentPropertiesDialogOptions && state.noteContentPropertiesDialogOptions.visible ? 'VISIBLE_DIALOGS_ADD' : 'VISIBLE_DIALOGS_REMOVE', - name: 'noteContentProperties', - }); + const onFocusCallback = () => { + dialogs.setActiveWindow(containerWindow); + }; + if (containerWindow.document.hasFocus()) { + onFocusCallback(); } - if (state.shareNoteDialogOptions !== prevState.shareNoteDialogOptions) { - props.dispatch({ - type: state.shareNoteDialogOptions && state.shareNoteDialogOptions.visible ? 'VISIBLE_DIALOGS_ADD' : 'VISIBLE_DIALOGS_REMOVE', - name: 'shareNote', - }); - } + containerWindow.addEventListener('focus', onFocusCallback); - if (state.shareFolderDialogOptions !== prevState.shareFolderDialogOptions) { - props.dispatch({ - type: state.shareFolderDialogOptions && state.shareFolderDialogOptions.visible ? 'VISIBLE_DIALOGS_ADD' : 'VISIBLE_DIALOGS_REMOVE', - name: 'shareFolder', - }); - } - lastDialogStateRef.current = dialogState; - }, [dialogState, props.dispatch]); + return () => { + containerWindow.removeEventListener('focus', onFocusCallback); + }; + }, [containerWindow]); +}; - const onPrintCallback = usePrintToCallback({ - customCss: props.customCss, - editorNoteStatuses: props.editorNoteStatuses, - plugins: props.plugins, - }); - const windowControl = useWindowControl(setDialogState, onPrintCallback); +const WindowCommandHandler: React.FC = props => { + const [referenceElement, setReferenceElement] = useState(null); + const containerDocument = useDom(referenceElement); const documentRef = useRef(null); - useNowEffect(() => { - const runtimeHandles = commands.map((command: ComponentCommandSpec) => { - const runtime: CommandRuntime = { - getPriority: () => { - return documentRef.current?.hasFocus() ? 1 : 0; - }, - ...command.runtime(windowControl), - }; - return CommandService.instance().registerRuntime( - command.declaration.name, - runtime, - true, - ); - }); + documentRef.current = containerDocument; - return () => { - for (const runtimeHandle of runtimeHandles) { - runtimeHandle.deregister(); - } - }; - }, [windowControl]); + const [dialogState, setDialogState] = useState(defaultDialogState); + + useSyncDialogState(dialogState, props.dispatch); + useWindowCommands({ + documentRef, + customCss: props.customCss, + plugins: props.plugins, + editorNoteStatuses: props.editorNoteStatuses, + setDialogState, + }); + useSyncActiveWindow(containerDocument?.defaultView); const onDialogHideCallbacks = useMemo(() => { type OnHideCallbacks = Partial void>>; @@ -136,63 +106,10 @@ const WindowCommandHandler: React.FC = props => { return result; }, []); - const renderPluginDialogs = () => { - const output = []; - const infos = pluginUtils.viewInfosByType(props.plugins, 'webview'); - - for (const info of infos) { - const { plugin, view } = info; - if (view.containerType !== ContainerType.Dialog) continue; - if (!props.currentWindowDialogs[view.id]) continue; - const html = props.pluginHtmlContents[plugin.id]?.[view.id] ?? ''; - - output.push(); - } - - if (!output.length) return null; - - return ( -
    - {output} -
    - ); - }; - - const renderAppDialogs = () => { - if (!props.appDialogs.length) return null; - - const output: React.ReactNode[] = []; - for (const dialog of props.appDialogs) { - const md = appDialogs[dialog.name]; - if (!md) throw new Error(`Unknown dialog: ${dialog.name}`); - output.push(md.render({ - key: dialog.name, - themeId: props.themeId, - dispatch: props.dispatch, - }, dialog.props)); - } - return output; - }; - const promptOnClose = useCallback((answer: unknown, buttonType: unknown) => { dialogState.promptOptions.onClose(answer, buttonType); }, [dialogState.promptOptions]); - const onReferenceElementLoad = useCallback((element: HTMLDivElement|null) => { - if (!element) return; - - documentRef.current ??= element.getRootNode() as Document; - }, []); - const dialogInfo = PluginManager.instance().pluginDialogToShow(props.pluginsLegacy); const pluginDialog = !dialogInfo ? null : ; @@ -200,10 +117,20 @@ const WindowCommandHandler: React.FC = props => { return <> -
    +
    {pluginDialog} {props.modalMessage !== null ? : null} - {renderPluginDialogs()} + + {noteContentPropertiesDialogOptions.visible && ( = props => { onClose={onDialogHideCallbacks.shareFolderDialogOptions} /> )} - {renderAppDialogs()} { return { themeId: state.settings.theme, plugins: state.pluginService.plugins, - currentWindowDialogs: windowState.visibleDialogs, - appDialogs: windowState.dialogs, + visibleDialogs: windowState.visibleDialogs, + appDialogStates: windowState.dialogs, pluginHtmlContents: state.pluginService.pluginHtmlContents, customCss: state.customViewerCss, editorNoteStatuses: state.editorNoteStatuses, diff --git a/packages/app-desktop/gui/WindowCommandHandler/utils/useSyncDialogState.ts b/packages/app-desktop/gui/WindowCommandHandler/utils/useSyncDialogState.ts new file mode 100644 index 00000000000..3caf3c9a569 --- /dev/null +++ b/packages/app-desktop/gui/WindowCommandHandler/utils/useSyncDialogState.ts @@ -0,0 +1,41 @@ +import { useEffect, useRef } from 'react'; +import { DialogState } from '../types'; +import { Dispatch } from 'redux'; + +const useSyncDialogState = (dialogState: DialogState, dispatch: Dispatch) => { + const lastDialogStateRef = useRef(dialogState); + useEffect(() => { + const prevState = lastDialogStateRef.current; + const state = dialogState; + if (state.notePropertiesDialogOptions !== prevState.notePropertiesDialogOptions) { + dispatch({ + type: state.notePropertiesDialogOptions && state.notePropertiesDialogOptions.visible ? 'VISIBLE_DIALOGS_ADD' : 'VISIBLE_DIALOGS_REMOVE', + name: 'noteProperties', + }); + } + + if (state.noteContentPropertiesDialogOptions !== prevState.noteContentPropertiesDialogOptions) { + dispatch({ + type: state.noteContentPropertiesDialogOptions && state.noteContentPropertiesDialogOptions.visible ? 'VISIBLE_DIALOGS_ADD' : 'VISIBLE_DIALOGS_REMOVE', + name: 'noteContentProperties', + }); + } + + if (state.shareNoteDialogOptions !== prevState.shareNoteDialogOptions) { + dispatch({ + type: state.shareNoteDialogOptions && state.shareNoteDialogOptions.visible ? 'VISIBLE_DIALOGS_ADD' : 'VISIBLE_DIALOGS_REMOVE', + name: 'shareNote', + }); + } + + if (state.shareFolderDialogOptions !== prevState.shareFolderDialogOptions) { + dispatch({ + type: state.shareFolderDialogOptions && state.shareFolderDialogOptions.visible ? 'VISIBLE_DIALOGS_ADD' : 'VISIBLE_DIALOGS_REMOVE', + name: 'shareFolder', + }); + } + lastDialogStateRef.current = dialogState; + }, [dialogState, dispatch]); +}; + +export default useSyncDialogState; diff --git a/packages/app-desktop/gui/WindowCommandHandler/utils/useWindowCommands.ts b/packages/app-desktop/gui/WindowCommandHandler/utils/useWindowCommands.ts new file mode 100644 index 00000000000..36240d66961 --- /dev/null +++ b/packages/app-desktop/gui/WindowCommandHandler/utils/useWindowCommands.ts @@ -0,0 +1,49 @@ +import { EditorNoteStatuses } from '@joplin/lib/reducer'; +import { RefObject } from 'react'; +import usePrintToCallback from './usePrintToCallback'; +import useWindowControl, { OnSetDialogState, WindowControl } from './useWindowControl'; +import commands from '../commands'; +import CommandService, { CommandRuntime, ComponentCommandSpec } from '@joplin/lib/services/CommandService'; +import useNowEffect from '@joplin/lib/hooks/useNowEffect'; +import { PluginStates } from '@joplin/lib/services/plugins/reducer'; + +interface Props { + documentRef: RefObject; + customCss: string; + plugins: PluginStates; + editorNoteStatuses: EditorNoteStatuses; + setDialogState: OnSetDialogState; +} + +const useWindowCommands = ({ documentRef, customCss, plugins, editorNoteStatuses, setDialogState }: Props) => { + const onPrintCallback = usePrintToCallback({ + customCss: customCss, + editorNoteStatuses: editorNoteStatuses, + plugins: plugins, + }); + const windowControl = useWindowControl(setDialogState, onPrintCallback); + + useNowEffect(() => { + const runtimeHandles = commands.map((command: ComponentCommandSpec) => { + const runtime: CommandRuntime = { + getPriority: () => { + return documentRef.current?.hasFocus() ? 1 : 0; + }, + ...command.runtime(windowControl), + }; + return CommandService.instance().registerRuntime( + command.declaration.name, + runtime, + true, + ); + }); + + return () => { + for (const runtimeHandle of runtimeHandles) { + runtimeHandle.deregister(); + } + }; + }, [windowControl]); +}; + +export default useWindowCommands; diff --git a/packages/app-desktop/gui/WindowCommandHandler/utils/useWindowControl.ts b/packages/app-desktop/gui/WindowCommandHandler/utils/useWindowControl.ts index 45c624e53ae..4aa6b2558d4 100644 --- a/packages/app-desktop/gui/WindowCommandHandler/utils/useWindowControl.ts +++ b/packages/app-desktop/gui/WindowCommandHandler/utils/useWindowControl.ts @@ -8,7 +8,7 @@ export interface WindowControl { printTo: PrintCallback; } -type OnSetDialogState = React.Dispatch>; +export type OnSetDialogState = React.Dispatch>; const useWindowControl = (setDialogState: OnSetDialogState, onPrint: PrintCallback) => { // Avoid reloading the output where possible -- reloading the window control // may mean reloading all main window commands. diff --git a/packages/app-desktop/gui/dialogs.ts b/packages/app-desktop/gui/dialogs.ts index b05f55eebfb..0f65056107b 100644 --- a/packages/app-desktop/gui/dialogs.ts +++ b/packages/app-desktop/gui/dialogs.ts @@ -6,15 +6,33 @@ const smalltalk = require('smalltalk'); const logger = Logger.create('dialogs'); +interface Smalltalk { + alert(title: string, message: string): Promise; + confirm(title: string, message: string, options: unknown): Promise; + prompt(title: string, message: string, defaultValue: string, options: unknown): Promise; +} + class Dialogs { + private activeWindow: Window|null; + public setActiveWindow(win: Window) { + this.activeWindow = win; + } + + private get smalltalk(): Smalltalk { + if (this.activeWindow && 'smalltalk' in this.activeWindow) { + return this.activeWindow.smalltalk as Smalltalk; + } + return smalltalk; + } + public async alert(message: string, title = '') { - await smalltalk.alert(title, message); + await this.smalltalk.alert(title, message); } // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied public async confirm(message: string, title = '', options: any = {}) { try { - await smalltalk.confirm(title, message, options); + await this.smalltalk.confirm(title, message, options); return true; } catch (error) { logger.error(error); @@ -28,7 +46,7 @@ class Dialogs { try { // https://github.com/laurent22/joplin/pull/10258#discussion_r1550306545 - const answer = await smalltalk.prompt(title, message, defaultValue, options); + const answer = await this.smalltalk.prompt(title, message, defaultValue, options); return answer; } catch (error) { logger.warn('Prompt appears to have been cancelled:', error); diff --git a/packages/lib/reducer.ts b/packages/lib/reducer.ts index d3cc7f2e5fe..995391b5096 100644 --- a/packages/lib/reducer.ts +++ b/packages/lib/reducer.ts @@ -116,6 +116,10 @@ export const defaultWindowState: WindowState = { }, }; +export interface EditorNoteStatuses { + [id: string]: string; +} + export interface State extends WindowState { backgroundWindows: Record; @@ -151,8 +155,7 @@ export interface State extends WindowState { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied pluginsLegacy: any; provisionalNoteIds: string[]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - editorNoteStatuses: any; + editorNoteStatuses: EditorNoteStatuses; isInsertingNotes: boolean; hasEncryptedItems: boolean; needApiAuth: boolean; From b76b42a64de7e605e81450080882557c7a89511e Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 1 Nov 2024 11:47:35 -0700 Subject: [PATCH 69/97] Fix smalltalk dialogs in secondary windows --- .../app-desktop/gui/NewWindowOrIFrame.tsx | 58 ++++++++++++++----- packages/app-desktop/gui/dialogs.ts | 11 ++-- packages/app-desktop/index.html | 2 +- .../tools/copyApplicationAssets.js | 1 + 4 files changed, 51 insertions(+), 21 deletions(-) diff --git a/packages/app-desktop/gui/NewWindowOrIFrame.tsx b/packages/app-desktop/gui/NewWindowOrIFrame.tsx index dfae971ebd7..048e1fd2648 100644 --- a/packages/app-desktop/gui/NewWindowOrIFrame.tsx +++ b/packages/app-desktop/gui/NewWindowOrIFrame.tsx @@ -11,6 +11,9 @@ export enum WindowMode { export const WindowIdContext = createContext(defaultWindowId); +type OnCloseCallback = ()=> void; +type OnFocusCallback = ()=> void; + interface Props { // Note: children will be rendered in a different DOM from this node. Avoid using document.* methods // in child components. @@ -18,26 +21,26 @@ interface Props { title: string; mode: WindowMode; windowId: string; - onClose: ()=> void; - onFocus?: ()=> void; + onClose: OnCloseCallback; + onFocus?: OnFocusCallback; } -const NewWindowOrIFrame: React.FC = props => { - const [iframeRef, setIframeRef] = useState(null); - const [loaded, setLoaded] = useState(false); +const useDocument = ( + mode: WindowMode, + iframeElement: HTMLIFrameElement|null, + onClose: OnCloseCallback, +) => { const [doc, setDoc] = useState(null); - const onCloseRef = useRef(props.onClose); - onCloseRef.current = props.onClose; - const onFocusRef = useRef(props.onFocus); - onFocusRef.current = props.onFocus; + const onCloseRef = useRef(onClose); + onCloseRef.current = onClose; useEffect(() => { let openedWindow: Window|null = null; const unmounted = false; - if (iframeRef) { - setDoc(iframeRef?.contentWindow?.document); - } else if (props.mode === WindowMode.NewWindow) { + if (iframeElement) { + setDoc(iframeElement?.contentWindow?.document); + } else if (mode === WindowMode.NewWindow) { openedWindow = window.open('about:blank'); setDoc(openedWindow.document); @@ -68,11 +71,19 @@ const NewWindowOrIFrame: React.FC = props => { } }, 200); - if (iframeRef && !openedWindow) { + if (iframeElement && !openedWindow) { onCloseRef.current?.(); } }; - }, [iframeRef, props.mode]); + }, [iframeElement, mode]); + + return doc; +}; + +type OnSetLoaded = (loaded: boolean)=> void; +const useDocumentSetup = (doc: Document|null, setLoaded: OnSetLoaded, onFocus?: OnFocusCallback) => { + const onFocusRef = useRef(onFocus); + onFocusRef.current = onFocus; useEffect(() => { if (!doc) return; @@ -98,6 +109,15 @@ const NewWindowOrIFrame: React.FC = props => { doc.head.appendChild(style); } + const jsUrls = [ + 'vendor/lib/smalltalk/dist/smalltalk.min.js', + ]; + for (const url of jsUrls) { + const script = doc.createElement('script'); + script.src = url; + doc.head.appendChild(script); + } + doc.body.style.height = '100vh'; const containerWindow = doc.defaultView; @@ -109,7 +129,15 @@ const NewWindowOrIFrame: React.FC = props => { } setLoaded(true); - }, [doc]); + }, [doc, setLoaded]); +}; + +const NewWindowOrIFrame: React.FC = props => { + const [iframeRef, setIframeRef] = useState(null); + const [loaded, setLoaded] = useState(false); + + const doc = useDocument(props.mode, iframeRef, props.onClose); + useDocumentSetup(doc, setLoaded, props.onFocus); useEffect(() => { if (!doc) return; diff --git a/packages/app-desktop/gui/dialogs.ts b/packages/app-desktop/gui/dialogs.ts index 0f65056107b..f8185bb5cae 100644 --- a/packages/app-desktop/gui/dialogs.ts +++ b/packages/app-desktop/gui/dialogs.ts @@ -1,9 +1,5 @@ import Logger from '@joplin/utils/Logger'; -// Can't upgrade beyond 2.x because it doesn't work with Electron. If trying to -// upgrade again, check that adding a link from the CodeMirror editor works/ -const smalltalk = require('smalltalk'); - const logger = Logger.create('dialogs'); interface Smalltalk { @@ -22,7 +18,12 @@ class Dialogs { if (this.activeWindow && 'smalltalk' in this.activeWindow) { return this.activeWindow.smalltalk as Smalltalk; } - return smalltalk; + + if ('smalltalk' in window) { + return window.smalltalk as Smalltalk; + } + + throw new Error('Unable to find the smalltalk library. Please make sure it has been loaded with a diff --git a/packages/app-desktop/tools/copyApplicationAssets.js b/packages/app-desktop/tools/copyApplicationAssets.js index 5049ad01366..a6c7bb5ffae 100644 --- a/packages/app-desktop/tools/copyApplicationAssets.js +++ b/packages/app-desktop/tools/copyApplicationAssets.js @@ -88,6 +88,7 @@ async function main() { 'react-datetime/css/react-datetime.css', 'roboto-fontface/css/roboto/roboto-fontface.css', 'smalltalk/css/smalltalk.css', + 'smalltalk/dist/smalltalk.min.js', 'smalltalk/img/IDR_CLOSE_DIALOG_H.png', 'smalltalk/img/IDR_CLOSE_DIALOG.png', { From f3812d2bac422525a804e2da833f51e0d03d021e Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 1 Nov 2024 11:55:12 -0700 Subject: [PATCH 70/97] Trying to fix CI --- packages/app-mobile/package.json | 1 + yarn.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/app-mobile/package.json b/packages/app-mobile/package.json index 7867cc36745..10f264defe9 100644 --- a/packages/app-mobile/package.json +++ b/packages/app-mobile/package.json @@ -100,6 +100,7 @@ "@tsconfig/react-native": "2.0.2", "@types/fs-extra": "11.0.4", "@types/jest": "29.5.12", + "@types/node": "18.19.42", "@types/react": "18.3.3", "@types/react-native": "0.70.6", "@types/react-redux": "7.1.33", diff --git a/yarn.lock b/yarn.lock index 39c768a1df6..b5a6f3fb6de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8223,6 +8223,7 @@ __metadata: "@tsconfig/react-native": 2.0.2 "@types/fs-extra": 11.0.4 "@types/jest": 29.5.12 + "@types/node": 18.19.42 "@types/react": 18.3.3 "@types/react-native": 0.70.6 "@types/react-redux": 7.1.33 From 5b5706aac28de49b803f41ce4afb4487800020f5 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 1 Nov 2024 11:57:43 -0700 Subject: [PATCH 71/97] Trying to fix CI In CI, there seems to be a mismatch between `@types/node` in packages/app-mobile and packages/lib, causing build failures. Interestingly, these failures aren't happening locally. --- yarn.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index dc9b17529fd..424d485f831 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13084,6 +13084,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:18.19.42, @types/node@npm:^18.0.0": + version: 18.19.42 + resolution: "@types/node@npm:18.19.42" + dependencies: + undici-types: ~5.26.4 + checksum: 3f976583d3f4ff6040187f98e838337d59134e53bfe1cf241d8143e87e6f9507a1ad0aa435ea550c21d76c6cabb78f63a410413de476764f45695378cc022377 + languageName: node + linkType: hard + "@types/node@npm:18.19.43": version: 18.19.43 resolution: "@types/node@npm:18.19.43" @@ -13100,15 +13109,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^18.0.0": - version: 18.19.42 - resolution: "@types/node@npm:18.19.42" - dependencies: - undici-types: ~5.26.4 - checksum: 3f976583d3f4ff6040187f98e838337d59134e53bfe1cf241d8143e87e6f9507a1ad0aa435ea550c21d76c6cabb78f63a410413de476764f45695378cc022377 - languageName: node - linkType: hard - "@types/node@npm:^20.10.6": version: 20.11.16 resolution: "@types/node@npm:20.11.16" From d8abbda308e0b898aa440bc8a23a2fc6013cf03b Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 1 Nov 2024 12:04:30 -0700 Subject: [PATCH 72/97] Update background_windows.md --- readme/dev/spec/background_windows.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme/dev/spec/background_windows.md b/readme/dev/spec/background_windows.md index cd634a7841d..e0d1f96fc3d 100644 --- a/readme/dev/spec/background_windows.md +++ b/readme/dev/spec/background_windows.md @@ -1,6 +1,6 @@ # Background windows -On desktop, Joplin supports multiple windows using the `backgroundWindows` reducer property. Each entry in `backgroundWindows` contains the state of a window that is in the background (not currently focused). +On desktop, Joplin supports multiple windows with the `backgroundWindows` reducer state. Each entry in `backgroundWindows` contains the state of a window that is in the background (not currently focused). The currently focused window's state is stored in the top-level state object. ## State structure From b8a5ae641cdddae797e1811424db4255d2fdba2b Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 1 Nov 2024 12:07:22 -0700 Subject: [PATCH 73/97] Updating CI fix: Make app-mobile types/node match version of other types/node copies --- packages/app-mobile/package.json | 2 +- yarn.lock | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/app-mobile/package.json b/packages/app-mobile/package.json index 10f264defe9..a0eb518fe90 100644 --- a/packages/app-mobile/package.json +++ b/packages/app-mobile/package.json @@ -100,7 +100,7 @@ "@tsconfig/react-native": "2.0.2", "@types/fs-extra": "11.0.4", "@types/jest": "29.5.12", - "@types/node": "18.19.42", + "@types/node": "18.19.43", "@types/react": "18.3.3", "@types/react-native": "0.70.6", "@types/react-redux": "7.1.33", diff --git a/yarn.lock b/yarn.lock index 424d485f831..f19a74a0aac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8223,7 +8223,7 @@ __metadata: "@tsconfig/react-native": 2.0.2 "@types/fs-extra": 11.0.4 "@types/jest": 29.5.12 - "@types/node": 18.19.42 + "@types/node": 18.19.43 "@types/react": 18.3.3 "@types/react-native": 0.70.6 "@types/react-redux": 7.1.33 @@ -13084,15 +13084,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:18.19.42, @types/node@npm:^18.0.0": - version: 18.19.42 - resolution: "@types/node@npm:18.19.42" - dependencies: - undici-types: ~5.26.4 - checksum: 3f976583d3f4ff6040187f98e838337d59134e53bfe1cf241d8143e87e6f9507a1ad0aa435ea550c21d76c6cabb78f63a410413de476764f45695378cc022377 - languageName: node - linkType: hard - "@types/node@npm:18.19.43": version: 18.19.43 resolution: "@types/node@npm:18.19.43" @@ -13109,6 +13100,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^18.0.0": + version: 18.19.42 + resolution: "@types/node@npm:18.19.42" + dependencies: + undici-types: ~5.26.4 + checksum: 3f976583d3f4ff6040187f98e838337d59134e53bfe1cf241d8143e87e6f9507a1ad0aa435ea550c21d76c6cabb78f63a410413de476764f45695378cc022377 + languageName: node + linkType: hard + "@types/node@npm:^20.10.6": version: 20.11.16 resolution: "@types/node@npm:20.11.16" From 34ea5b51f4368188c625143293feb00064afabc5 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 1 Nov 2024 12:44:04 -0700 Subject: [PATCH 74/97] Changes from self-review of code --- packages/app-desktop/bridge.ts | 23 ++--- packages/app-desktop/gui/PdfViewer.tsx | 2 +- .../gui/Sidebar/hooks/useOnRenderItem.tsx | 4 +- .../listItemComponents/AllNotesItem.tsx | 2 +- .../Sidebar/listItemComponents/HeaderItem.tsx | 2 +- .../gui/StyleSheets/StyleSheetContainer.tsx | 85 +++++++++++-------- packages/app-desktop/gui/ToolbarBase.tsx | 11 +-- .../commands/showSpellCheckerMenu.ts | 2 +- .../utils/useSyncDialogState.ts | 1 + .../utils/useWindowCommands.ts | 2 + .../utils/useWindowControl.ts | 2 +- packages/app-desktop/gui/dialogs.ts | 3 + packages/app-desktop/main.scss | 9 -- 13 files changed, 76 insertions(+), 72 deletions(-) diff --git a/packages/app-desktop/bridge.ts b/packages/app-desktop/bridge.ts index 1ca6168acbc..0a25fffe8b9 100644 --- a/packages/app-desktop/bridge.ts +++ b/packages/app-desktop/bridge.ts @@ -294,18 +294,13 @@ export class Bridge { return new BrowserWindow(options); } + // Note: This provides the size of the main window. Prefer CSS where possible. public windowContentSize() { if (!this.mainWindow()) return { width: 0, height: 0 }; const s = this.mainWindow().getContentSize(); return { width: s[0], height: s[1] }; } - public windowSize() { - if (!this.mainWindow()) return { width: 0, height: 0 }; - const s = this.mainWindow().getSize(); - return { width: s[0], height: s[1] }; - } - public windowSetSize(width: number, height: number) { if (!this.mainWindow()) return; return this.mainWindow().setSize(width, height); @@ -323,7 +318,7 @@ export class Bridge { public async showSaveDialog(options: any) { if (!options) options = {}; if (!('defaultPath' in options) && this.lastSelectedPaths_.file) options.defaultPath = this.lastSelectedPaths_.file; - const { filePath } = await dialog.showSaveDialog(this.mainWindow(), options); + const { filePath } = await dialog.showSaveDialog(this.activeWindow(), options); if (filePath) { this.lastSelectedPaths_.file = filePath; } @@ -338,7 +333,7 @@ export class Bridge { if (!('defaultPath' in options) && (this.lastSelectedPaths_ as any)[fileType]) options.defaultPath = (this.lastSelectedPaths_ as any)[fileType]; if (!('createDirectory' in options)) options.createDirectory = true; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - const { filePaths } = await dialog.showOpenDialog(this.mainWindow(), options as any); + const { filePaths } = await dialog.showOpenDialog(this.activeWindow(), options as any); if (filePaths && filePaths.length) { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied (this.lastSelectedPaths_ as any)[fileType] = dirname(filePaths[0]); @@ -349,7 +344,7 @@ export class Bridge { // Don't use this directly - call one of the showXxxxxxxMessageBox() instead // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied private showMessageBox_(window: any, options: MessageDialogOptions): number { - if (!window) window = this.mainWindow(); + if (!window) window = this.activeWindow(); return dialog.showMessageBoxSync(window, { message: '', ...options }); } @@ -359,7 +354,7 @@ export class Bridge { ...options, }; - return this.showMessageBox_(this.mainWindow(), { + return this.showMessageBox_(this.activeWindow(), { type: 'error', message: message, buttons: options.buttons, @@ -372,7 +367,7 @@ export class Bridge { ...options, }; - const result = this.showMessageBox_(this.mainWindow(), { type: 'question', + const result = this.showMessageBox_(this.activeWindow(), { type: 'question', message: message, cancelId: 1, buttons: options.buttons, ...options }); @@ -382,7 +377,7 @@ export class Bridge { /* returns the index of the clicked button */ public showMessageBox(message: string, options: MessageDialogOptions = {}) { - const result = this.showMessageBox_(this.mainWindow(), { type: 'question', + const result = this.showMessageBox_(this.activeWindow(), { type: 'question', message: message, buttons: [_('OK'), _('Cancel')], ...options }); @@ -391,7 +386,7 @@ export class Bridge { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied public showInfoMessageBox(message: string, options: any = {}) { - const result = this.showMessageBox_(this.mainWindow(), { type: 'info', + const result = this.showMessageBox_(this.activeWindow(), { type: 'info', message: message, buttons: [_('OK')], ...options }); return result === 0; @@ -435,7 +430,7 @@ export class Bridge { const allowOpenId = 2; const learnMoreId = 1; const fileExtensionDescription = JSON.stringify(fileExtension); - const result = await dialog.showMessageBox(this.mainWindow(), { + const result = await dialog.showMessageBox(this.activeWindow(), { title: _('Unknown file type'), message: _('Joplin doesn\'t recognise the %s extension. Opening this file could be dangerous. What would you like to do?', fileExtensionDescription), diff --git a/packages/app-desktop/gui/PdfViewer.tsx b/packages/app-desktop/gui/PdfViewer.tsx index d899341a2e8..806615ea8b1 100644 --- a/packages/app-desktop/gui/PdfViewer.tsx +++ b/packages/app-desktop/gui/PdfViewer.tsx @@ -69,7 +69,7 @@ export default function PdfViewer(props: Props) { mdToHtml: async (_a, b, _c) => { return { html: b, pluginAssets: [], cssStrings: [] }; }, } as ContextMenuOptions, props.dispatch); - menu.popup({ window: bridge().mainWindow() }); + menu.popup({ window: bridge().activeWindow() }); }, [props.dispatch]); // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied diff --git a/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx b/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx index a23e687a476..f9fac470e21 100644 --- a/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx +++ b/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx @@ -118,7 +118,7 @@ const useOnRenderItem = (props: Props) => { menu.append( new MenuItem(menuUtils.commandToStatefulMenuItem('emptyTrash')), ); - menu.popup({ window: bridge().mainWindow() }); + menu.popup({ window: bridge().activeWindow() }); return; } @@ -268,7 +268,7 @@ const useOnRenderItem = (props: Props) => { } } - menu.popup({ window: bridge().mainWindow() }); + menu.popup({ window: bridge().activeWindow() }); }, [props.dispatch, pluginsRef]); diff --git a/packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.tsx b/packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.tsx index c25555e550f..f9f1bd928ec 100644 --- a/packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.tsx +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.tsx @@ -45,7 +45,7 @@ const AllNotesItem: React.FC = props => { })); } - menu.popup({ window: bridge().mainWindow() }); + menu.popup({ window: bridge().activeWindow() }); }, []); return ( diff --git a/packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.tsx b/packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.tsx index e59d37e58c9..a01d8a79495 100644 --- a/packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.tsx +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.tsx @@ -40,7 +40,7 @@ const HeaderItem: React.FC = props => { new MenuItem(menuUtils.commandToStatefulMenuItem('newFolder')), ); - menu.popup({ window: bridge().mainWindow() }); + menu.popup({ window: bridge().activeWindow() }); } }, [itemId]); diff --git a/packages/app-desktop/gui/StyleSheets/StyleSheetContainer.tsx b/packages/app-desktop/gui/StyleSheets/StyleSheetContainer.tsx index bb8e555c70e..8f0ab0a8d03 100644 --- a/packages/app-desktop/gui/StyleSheets/StyleSheetContainer.tsx +++ b/packages/app-desktop/gui/StyleSheets/StyleSheetContainer.tsx @@ -10,7 +10,7 @@ import * as React from 'react'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect'; import themeToCss from '@joplin/lib/services/style/themeToCss'; import { themeStyle } from '@joplin/lib/theme'; @@ -32,58 +32,42 @@ const editorFontFromSettings = (settingValue: string) => { return fontFamilies; }; -const StyleSheetContainer: React.FC = props => { - const [themeStyleSheetContent, setThemeStyleSheetContent] = useState(''); - const [editorStyleSheetContent, setEditorStyleSheetContent] = useState(''); - const [elementRef, setElementRef] = useState(null); - const doc = useDom(elementRef); +const useThemeCss = (themeId: number) => { + const [themeCss, setThemeCss] = useState(''); useAsyncEffect(async (event: AsyncEffectEvent) => { - const theme = themeStyle(props.themeId); + const theme = themeStyle(themeId); const themeCss = themeToCss(theme); if (event.cancelled) return; - setThemeStyleSheetContent(themeCss); - }, [props.themeId]); + setThemeCss(themeCss); + }, [themeId]); + return themeCss; +}; - useEffect(() => { - const fontFamilies = editorFontFromSettings(props.editorFontSetting); - setEditorStyleSheetContent(` +const useEditorCss = (editorFontSetting: string) => { + return useMemo(() => { + const fontFamilies = editorFontFromSettings(editorFontSetting); + return ` /* The '*' and '!important' parts are necessary to make sure Russian text is displayed properly https://github.com/laurent22/joplin/issues/155 Note: Be careful about the specificity here. Incorrect specificity can break monospaced fonts in tables. */ .CodeMirror5 *, .cm-editor .cm-content { font-family: ${fontFamilies.join(', ')} !important; } - `); - }, [props.editorFontSetting]); - - useEffect(() => { - if (!doc) return () => {}; - - const element = doc.createElement('style'); - element.setAttribute('id', 'main-theme-stylesheet-container'); - doc.head.appendChild(element); - element.appendChild(document.createTextNode(` - /* Theme CSS */ - ${themeStyleSheetContent} - - /* Editor font CSS */ - ${editorStyleSheetContent} - `)); - return () => { - doc.head.removeChild(element); - }; - }, [themeStyleSheetContent, editorStyleSheetContent, doc]); + `; + }, [editorFontSetting]); +}; +const useLinkedCss = (doc: Document|null, cssPaths: string[]) => { useEffect(() => { if (!doc) return () => {}; const elements: HTMLElement[] = []; - for (const path of props.customChromeCssPaths) { + for (const path of cssPaths) { const element = doc.createElement('link'); element.rel = 'stylesheet'; element.href = path; - element.classList.add('custom-userchrome-css'); + element.classList.add('dynamic-linked-stylesheet'); doc.head.appendChild(element); elements.push(element); @@ -94,7 +78,38 @@ const StyleSheetContainer: React.FC = props => { element.remove(); } }; - }, [doc, props.customChromeCssPaths]); + }, [doc, cssPaths]); +}; + +const useAppliedCss = (doc: Document|null, css: string) => { + useEffect(() => { + if (!doc) return () => {}; + + const element = doc.createElement('style'); + element.setAttribute('id', 'main-theme-stylesheet-container'); + doc.head.appendChild(element); + element.appendChild(document.createTextNode(css)); + return () => { + doc.head.removeChild(element); + }; + }, [css, doc]); +}; + +const StyleSheetContainer: React.FC = props => { + const [elementRef, setElementRef] = useState(null); + const doc = useDom(elementRef); + + const themeCss = useThemeCss(props.themeId); + const editorCss = useEditorCss(props.editorFontSetting); + + useAppliedCss(doc, ` + /* Theme CSS */ + ${themeCss} + + /* Editor font CSS */ + ${editorCss} + `); + useLinkedCss(doc, props.customChromeCssPaths); return
    ; }; diff --git a/packages/app-desktop/gui/ToolbarBase.tsx b/packages/app-desktop/gui/ToolbarBase.tsx index f55451a7ae0..83a5f3f01c9 100644 --- a/packages/app-desktop/gui/ToolbarBase.tsx +++ b/packages/app-desktop/gui/ToolbarBase.tsx @@ -7,7 +7,6 @@ import { AppState } from '../app.reducer'; import { connect } from 'react-redux'; import { useCallback, useMemo, useRef, useState } from 'react'; import { focus } from '@joplin/lib/utils/focusHandler'; -import useDom from './hooks/useDom'; interface ToolbarItemInfo extends ToolbarButtonInfo { type?: string; @@ -106,11 +105,9 @@ const ToolbarBaseComponent: React.FC = props => { const focusableItems = useMemo(() => { return allItems.filter(isFocusable); }, [allItems]); - const [containerNode, setContainerNode] = useState(null); - const containerRef = useRef(containerNode); - containerRef.current = containerNode; - const doc = useDom(containerNode); - const containerHasFocus = !!containerRef.current?.contains(doc.activeElement); + const containerRef = useRef(null); + const doc = containerRef.current?.ownerDocument; + const containerHasFocus = !!containerRef.current?.contains(doc?.activeElement); let keyCounter = 0; const renderItem = (o: ToolbarItemInfo, indexInFocusable: number) => { @@ -184,7 +181,7 @@ const ToolbarBaseComponent: React.FC = props => { return (
    { const menuItems = SpellCheckerService.instance().spellCheckerConfigMenuItems(selectedLanguages, useSpellChecker); // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied const menu = Menu.buildFromTemplate(menuItems as any); - menu.popup({ window: bridge().mainWindow() }); + menu.popup({ window: bridge().activeWindow() }); }, mapStateToTitle(state: AppState): string { diff --git a/packages/app-desktop/gui/WindowCommandHandler/utils/useSyncDialogState.ts b/packages/app-desktop/gui/WindowCommandHandler/utils/useSyncDialogState.ts index 3caf3c9a569..69f6d6ab332 100644 --- a/packages/app-desktop/gui/WindowCommandHandler/utils/useSyncDialogState.ts +++ b/packages/app-desktop/gui/WindowCommandHandler/utils/useSyncDialogState.ts @@ -2,6 +2,7 @@ import { useEffect, useRef } from 'react'; import { DialogState } from '../types'; import { Dispatch } from 'redux'; +// Syncs whether dialogs are open/closed with the global reducer state. const useSyncDialogState = (dialogState: DialogState, dispatch: Dispatch) => { const lastDialogStateRef = useRef(dialogState); useEffect(() => { diff --git a/packages/app-desktop/gui/WindowCommandHandler/utils/useWindowCommands.ts b/packages/app-desktop/gui/WindowCommandHandler/utils/useWindowCommands.ts index 36240d66961..595aa749778 100644 --- a/packages/app-desktop/gui/WindowCommandHandler/utils/useWindowCommands.ts +++ b/packages/app-desktop/gui/WindowCommandHandler/utils/useWindowCommands.ts @@ -23,6 +23,8 @@ const useWindowCommands = ({ documentRef, customCss, plugins, editorNoteStatuses }); const windowControl = useWindowControl(setDialogState, onPrintCallback); + // This effect needs to run as soon as possible. Certain components may fail to load if window + // commands are not registered on their first render. useNowEffect(() => { const runtimeHandles = commands.map((command: ComponentCommandSpec) => { const runtime: CommandRuntime = { diff --git a/packages/app-desktop/gui/WindowCommandHandler/utils/useWindowControl.ts b/packages/app-desktop/gui/WindowCommandHandler/utils/useWindowControl.ts index 4aa6b2558d4..b5b50ba2269 100644 --- a/packages/app-desktop/gui/WindowCommandHandler/utils/useWindowControl.ts +++ b/packages/app-desktop/gui/WindowCommandHandler/utils/useWindowControl.ts @@ -10,7 +10,7 @@ export interface WindowControl { export type OnSetDialogState = React.Dispatch>; const useWindowControl = (setDialogState: OnSetDialogState, onPrint: PrintCallback) => { - // Avoid reloading the output where possible -- reloading the window control + // Use refs to avoid reloading the output where possible -- reloading the window control // may mean reloading all main window commands. const onPrintRef = useRef(onPrint); onPrintRef.current = onPrint; diff --git a/packages/app-desktop/gui/dialogs.ts b/packages/app-desktop/gui/dialogs.ts index f8185bb5cae..4a57285759a 100644 --- a/packages/app-desktop/gui/dialogs.ts +++ b/packages/app-desktop/gui/dialogs.ts @@ -15,6 +15,9 @@ class Dialogs { } private get smalltalk(): Smalltalk { + // The smalltalk library shows prompts in whichever document it is loaded in + // (it uses the global document object). + // As such, a copy of the library needs to be loaded in each window: if (this.activeWindow && 'smalltalk' in this.activeWindow) { return this.activeWindow.smalltalk as Smalltalk; } diff --git a/packages/app-desktop/main.scss b/packages/app-desktop/main.scss index 516a4ac668e..399fede1c44 100644 --- a/packages/app-desktop/main.scss +++ b/packages/app-desktop/main.scss @@ -300,15 +300,6 @@ Component-specific classes // margin-right: 10px; // } -.master-password-dialog .fa-check { - color: var(--joplin-color-correct); -} - -.master-password-dialog .fa-times { - color: var(--joplin-color-error); - margin-left: 5px; -} - // Disable dragging of links (which are often buttons) a:not([draggable=true]), img:not([draggable=true]) { From 214a57829033064e31458944538ddc6b00f864f2 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 1 Nov 2024 12:44:28 -0700 Subject: [PATCH 75/97] Attempting to fix failing useFormNote test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test was failing in CI (but not locally). The original failure was: ➤ YN0000: [@joplin/app-desktop]: FAIL gui/NoteEditor/utils/useFormNote.test.ts (5.586 s) ➤ YN0000: [@joplin/app-desktop]: ● useFormNote › should update note when decryption completes ➤ YN0000: [@joplin/app-desktop]: ➤ YN0000: [@joplin/app-desktop]: Timed out in waitFor after 1000ms. ➤ YN0000: [@joplin/app-desktop]: ➤ YN0000: [@joplin/app-desktop]: 70 | ➤ YN0000: [@joplin/app-desktop]: 71 | // Ending decryption should also cause a re-render ➤ YN0000: [@joplin/app-desktop]: > 72 | await formNote.waitFor(() => { ➤ YN0000: [@joplin/app-desktop]: | ^ ➤ YN0000: [@joplin/app-desktop]: 73 | expect(formNote.result.current.formNote).toMatchObject({ ➤ YN0000: [@joplin/app-desktop]: 74 | encryption_applied: 0, ➤ YN0000: [@joplin/app-desktop]: 75 | title: 'Test Note!', ➤ YN0000: [@joplin/app-desktop]: ➤ YN0000: [@joplin/app-desktop]: at Object.waitFor (node_modules/@testing-library/react-hooks/lib/core/asyncUtils.js:64:13) ➤ YN0000: [@joplin/app-desktop]: at Object. (gui/NoteEditor/utils/useFormNote.test.ts:72:3) ➤ YN0000: [@joplin/app-desktop]: --- .../gui/NoteEditor/utils/useFormNote.test.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.ts b/packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.ts index be23ea58e18..b792bada384 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.ts @@ -37,10 +37,12 @@ describe('useFormNote', () => { initialProps: makeFormNoteProps(), }); await formNote.waitFor(() => { - expect(formNote.result.current.formNote).toMatchObject({ - encryption_applied: 0, - title: testNote.title, - }); + // id is falsy until after the first load of the form note. + expect(formNote.result.current.formNote.id).not.toBeFalsy(); + }); + expect(formNote.result.current.formNote).toMatchObject({ + encryption_applied: 0, + title: testNote.title, }); await act(async () => { @@ -72,7 +74,6 @@ describe('useFormNote', () => { await formNote.waitFor(() => { expect(formNote.result.current.formNote).toMatchObject({ encryption_applied: 0, - title: 'Test Note!', }); }); From 3a00d7c132cdfa64f2822d5e280268af4a26d4f7 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 1 Nov 2024 12:58:02 -0700 Subject: [PATCH 76/97] Reducer refactoring and testing --- packages/app-desktop/app.reducer.test.ts | 26 +++++++++++++++++++++++- packages/app-desktop/app.reducer.ts | 12 +++++++---- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/app-desktop/app.reducer.test.ts b/packages/app-desktop/app.reducer.test.ts index d24b374a770..8f6f87e8bcc 100644 --- a/packages/app-desktop/app.reducer.test.ts +++ b/packages/app-desktop/app.reducer.test.ts @@ -1,4 +1,4 @@ -import { AppState } from './app.reducer'; +import { AppState, createAppDefaultWindowState } from './app.reducer'; import appReducer, { createAppDefaultState } from './app.reducer'; describe('app.reducer', () => { @@ -47,4 +47,28 @@ describe('app.reducer', () => { ]); }); + it('showing a dialog in one window should hide dialogs with the same ID in background windows', () => { + const state: AppState = { + ...createAppDefaultState({}, {}), + backgroundWindows: { + testWindow: { + ...createAppDefaultWindowState(), + windowId: 'testWindow', + + visibleDialogs: { + testDialog: true, + }, + }, + }, + }; + + const newState = appReducer(state, { + type: 'VISIBLE_DIALOGS_ADD', + name: 'testDialog', + }); + + expect(newState.backgroundWindows.testWindow.visibleDialogs).toEqual({}); + expect(newState.visibleDialogs).toEqual({ testDialog: true }); + }); + }); diff --git a/packages/app-desktop/app.reducer.ts b/packages/app-desktop/app.reducer.ts index a68c2c05cdb..3253ceee0f5 100644 --- a/packages/app-desktop/app.reducer.ts +++ b/packages/app-desktop/app.reducer.ts @@ -42,8 +42,12 @@ export interface AppWindowState extends WindowState { devToolsVisible: boolean; } +interface BackgroundWindowStates { + [windowId: string]: AppWindowState; +} + export interface AppState extends State, AppWindowState { - backgroundWindows: Record; + backgroundWindows: BackgroundWindowStates; route: AppStateRoute; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied @@ -100,7 +104,7 @@ export function createAppDefaultState(windowContentSize: any, resourceEditWatche }; } -const hideBackgroundDialogsMatching = produce((state: AppState, id: string) => { +const hideBackgroundDialogsWithId = produce((state: AppState, id: string) => { for (const windowId of Object.keys(state.backgroundWindows)) { const win = state.backgroundWindows[windowId]; if (id in win.visibleDialogs) { @@ -317,14 +321,14 @@ export default function(state: AppState, action: any) { newState = { ...state }; newState.visibleDialogs = { ...newState.visibleDialogs }; newState.visibleDialogs[action.name] = true; - newState = hideBackgroundDialogsMatching(newState, action.name); + newState = hideBackgroundDialogsWithId(newState, action.name); break; case 'VISIBLE_DIALOGS_REMOVE': newState = { ...state }; newState.visibleDialogs = { ...newState.visibleDialogs }; delete newState.visibleDialogs[action.name]; - newState = hideBackgroundDialogsMatching(newState, action.name); + newState = hideBackgroundDialogsWithId(newState, action.name); break; case 'FOCUS_SET': From 38ddf66dc9c6adc0c02619b36a42c0ee6430495a Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 1 Nov 2024 13:17:22 -0700 Subject: [PATCH 77/97] Fix Rich Text Editor context menu --- .../NoteBody/TinyMCE/utils/useContextMenu.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.ts index 7804237a2ac..c6230e4ca1b 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.ts +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.ts @@ -13,6 +13,7 @@ import Resource from '@joplin/lib/models/Resource'; import { TinyMceEditorEvents } from './types'; import { HtmlToMarkdownHandler, MarkupToHtmlHandler } from '../../../utils/types'; import { Editor } from 'tinymce'; +import { Menu } from 'electron'; const menuUtils = new MenuUtils(CommandService.instance()); @@ -55,12 +56,15 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Functio if (!editor) return () => {}; const contextMenuItems = menuItems(dispatch, htmlToMd, mdToHtml); + const targetWindow = bridge().activeWindow(); // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - function onContextMenu(_event: any, params: any) { + function onContextMenu(event: any, params: any) { const element = contextMenuElement(editor, params.x, params.y); if (!element) return; + event.preventDefault(); + let itemType: ContextMenuItemType = ContextMenuItemType.None; let resourceId = ''; let linkToCopy = null; @@ -118,15 +122,15 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Functio template = template.concat(menuUtils.pluginContextMenuItems(plugins, MenuItemLocation.EditorContextMenu)); - const menu = bridge().Menu.buildFromTemplate(template); + const menu = Menu.buildFromTemplate(template); menu.popup(); } - bridge().activeWindow().webContents.on('context-menu', onContextMenu); + targetWindow.webContents.on('context-menu', onContextMenu); return () => { - if (bridge().activeWindow()?.webContents?.off) { - bridge().activeWindow().webContents.off('context-menu', onContextMenu); + if (!targetWindow.isDestroyed() && targetWindow?.webContents?.off) { + targetWindow.webContents.off('context-menu', onContextMenu); } }; }, [editor, plugins, dispatch, htmlToMd, mdToHtml]); From 7c7f15a3b51c4c060213d4c990da1da9928af321 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 1 Nov 2024 13:39:31 -0700 Subject: [PATCH 78/97] Fix MacOS context menu shown in wrong window --- .../NoteBody/TinyMCE/utils/useContextMenu.ts | 25 +++++++++++-------- .../services/electron-context-menu.js | 4 ++- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.ts index c6230e4ca1b..b415f230ed6 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.ts +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.ts @@ -8,13 +8,15 @@ import { menuItems } from '../../../utils/contextMenu'; import MenuUtils from '@joplin/lib/services/commands/MenuUtils'; import CommandService from '@joplin/lib/services/CommandService'; import Setting from '@joplin/lib/models/Setting'; +import type { Event as ElectronEvent } from 'electron'; import Resource from '@joplin/lib/models/Resource'; import { TinyMceEditorEvents } from './types'; import { HtmlToMarkdownHandler, MarkupToHtmlHandler } from '../../../utils/types'; import { Editor } from 'tinymce'; -import { Menu } from 'electron'; +const Menu = bridge().Menu; +const MenuItem = bridge().MenuItem; const menuUtils = new MenuUtils(CommandService.instance()); // x and y are the absolute coordinates, as returned by the context-menu event @@ -59,12 +61,14 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Functio const targetWindow = bridge().activeWindow(); // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - function onContextMenu(event: any, params: any) { + function onContextMenu(event: ElectronEvent, params: any) { const element = contextMenuElement(editor, params.x, params.y); if (!element) return; event.preventDefault(); + const menu = new Menu(); + let itemType: ContextMenuItemType = ContextMenuItemType.None; let resourceId = ''; let linkToCopy = null; @@ -99,34 +103,33 @@ export default function(editor: Editor, plugins: PluginStates, dispatch: Functio mdToHtml, }; - let template = []; - for (const itemName in contextMenuItems) { const item = contextMenuItems[itemName]; if (!item.isActive(itemType, contextMenuActionOptions.current)) continue; - template.push({ + menu.append(new MenuItem({ label: item.label, click: () => { item.onAction(contextMenuActionOptions.current); }, - }); + })); } const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions); for (const item of spellCheckerMenuItems) { - template.push(item); + menu.append(new MenuItem(item)); } - template = template.concat(menuUtils.pluginContextMenuItems(plugins, MenuItemLocation.EditorContextMenu)); + for (const item of menuUtils.pluginContextMenuItems(plugins, MenuItemLocation.EditorContextMenu)) { + menu.append(new MenuItem(item)); + } - const menu = Menu.buildFromTemplate(template); - menu.popup(); + menu.popup({ window: targetWindow }); } - targetWindow.webContents.on('context-menu', onContextMenu); + targetWindow.webContents.prependListener('context-menu', onContextMenu); return () => { if (!targetWindow.isDestroyed() && targetWindow?.webContents?.off) { diff --git a/packages/app-desktop/services/electron-context-menu.js b/packages/app-desktop/services/electron-context-menu.js index 61a1e4fff6e..e8cb686e692 100644 --- a/packages/app-desktop/services/electron-context-menu.js +++ b/packages/app-desktop/services/electron-context-menu.js @@ -238,7 +238,9 @@ const create = (win, options) => { // When this is being called from a web view, we can't use `win` as this // would refer to the web view which is not allowed to render a popup menu. // - menu.popup({ window: electronRemote ? electronRemote.getCurrentWindow() : win }); + // Joplin change: Do not use electronRemote to get the current window -- this causes + // the menu to be shown on the wrong window in MacOS. + menu.popup({ window: win }); } }); }; From 6bce24c51bca4d58246c9542c96297898e06d51e Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 1 Nov 2024 14:12:53 -0700 Subject: [PATCH 79/97] Attempt to fix quickly switching windows can lead to invalid state if focus event is delayed --- .../app-desktop/gui/NoteEditor/EditorWindow.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/app-desktop/gui/NoteEditor/EditorWindow.tsx b/packages/app-desktop/gui/NoteEditor/EditorWindow.tsx index 8ee58aa087d..93c1f4e0cce 100644 --- a/packages/app-desktop/gui/NoteEditor/EditorWindow.tsx +++ b/packages/app-desktop/gui/NoteEditor/EditorWindow.tsx @@ -56,11 +56,14 @@ const SecondaryWindow: React.FC = props => { }, [props.dispatch, props.windowId, newWindow]); const onWindowFocus = useCallback(() => { - props.dispatch({ - type: 'WINDOW_FOCUS', - windowId: props.windowId, - lastWindowId: props.activeWindowId, - }); + // Verify that the window still has focus (e.g. to handle the case where the event was delayed). + if (containerRef.current?.ownerDocument.hasFocus()) { + props.dispatch({ + type: 'WINDOW_FOCUS', + windowId: props.windowId, + lastWindowId: props.activeWindowId, + }); + } }, [props.dispatch, props.windowId, props.activeWindowId]); return Date: Fri, 1 Nov 2024 14:50:13 -0700 Subject: [PATCH 80/97] Refactoring and commenting --- packages/app-desktop/gui/NewWindowOrIFrame.tsx | 11 ++++++++--- packages/app-desktop/gui/NoteEditor/NoteEditor.tsx | 4 ++-- packages/app-desktop/gui/NoteList/NoteList2.tsx | 4 ++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/app-desktop/gui/NewWindowOrIFrame.tsx b/packages/app-desktop/gui/NewWindowOrIFrame.tsx index 048e1fd2648..e27028ad71d 100644 --- a/packages/app-desktop/gui/NewWindowOrIFrame.tsx +++ b/packages/app-desktop/gui/NewWindowOrIFrame.tsx @@ -5,15 +5,20 @@ import { useState, useEffect, useRef, createContext } from 'react'; import { createPortal } from 'react-dom'; import { SecondaryWindowApi } from '../utils/window/types'; -export enum WindowMode { - Iframe, NewWindow, -} +// This component uses react-dom's Portals to render its children in a different HTML +// document. As children are rendered in a different Window/Document, they should avoid +// referencing the `window` and `document` globals. Instead, HTMLElement.ownerDocument +// and refs can be used to access the child component's DOM. export const WindowIdContext = createContext(defaultWindowId); type OnCloseCallback = ()=> void; type OnFocusCallback = ()=> void; +export enum WindowMode { + Iframe, NewWindow, +} + interface Props { // Note: children will be rendered in a different DOM from this node. Avoid using document.* methods // in child components. diff --git a/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx b/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx index 59f9c0a580f..6c1906a5d7b 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx @@ -630,11 +630,11 @@ function NoteEditorContent(props: NoteEditorProps) { ); } -interface OwnProps { +interface ConnectProps { windowId: string; } -const mapStateToProps = (state: AppState, ownProps: OwnProps) => { +const mapStateToProps = (state: AppState, ownProps: ConnectProps) => { const whenClauseContext = stateToWhenClauseContext(state, { windowId: ownProps.windowId }); const windowState = stateUtils.windowStateById(state, ownProps.windowId); const noteId = stateUtils.selectedNoteId(windowState); diff --git a/packages/app-desktop/gui/NoteList/NoteList2.tsx b/packages/app-desktop/gui/NoteList/NoteList2.tsx index a68668a7f8c..027e3f123e2 100644 --- a/packages/app-desktop/gui/NoteList/NoteList2.tsx +++ b/packages/app-desktop/gui/NoteList/NoteList2.tsx @@ -312,11 +312,11 @@ const NoteList = (props: Props) => { ); }; -interface OwnProps { +interface ConnectProps { windowId: string; } -const mapStateToProps = (state: AppState, ownProps: OwnProps) => { +const mapStateToProps = (state: AppState, ownProps: ConnectProps) => { const selectedFolder: FolderEntity = state.notesParentType === 'Folder' ? Folder.byId(state.folders, state.selectedFolderId) : null; const userId = state.settings['sync.userId']; const windowState = stateUtils.windowStateById(state, ownProps.windowId); From fbb3ba155332f3d1d03edc73ee505df84974db81 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 1 Nov 2024 16:42:16 -0700 Subject: [PATCH 81/97] Linux: Fix CodeMirror context menu --- .../NoteBody/CodeMirror/utils/useContextMenu.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.ts index 3c8cc1716c7..67f0378c427 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.ts +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.ts @@ -152,15 +152,18 @@ const useContextMenu = (props: ContextMenuProps) => { menu.append(new MenuItem(item)); }); - menu.popup(); + menu.popup({ window: bridge().activeWindow() }); } // Prepend the event listener so that it gets called before // the listener that shows the default menu. - bridge().activeWindow().webContents.prependListener('context-menu', onContextMenu); + const targetWindow = bridge().activeWindow(); + targetWindow.webContents.prependListener('context-menu', onContextMenu); return () => { - bridge().activeWindow().webContents.off('context-menu', onContextMenu); + if (!targetWindow.isDestroyed()) { + targetWindow.webContents.off('context-menu', onContextMenu); + } }; }, [ props.plugins, props.editorClassName, editorRef, props.containerRef, From 65b78a5b3f437fe620986aee0fde53ba5cb015cb Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 1 Nov 2024 17:37:06 -0700 Subject: [PATCH 82/97] Fix replaceMisspelling in secondary windows --- packages/app-desktop/commands/replaceMisspelling.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-desktop/commands/replaceMisspelling.ts b/packages/app-desktop/commands/replaceMisspelling.ts index bf4f5659957..4f37765df79 100644 --- a/packages/app-desktop/commands/replaceMisspelling.ts +++ b/packages/app-desktop/commands/replaceMisspelling.ts @@ -22,7 +22,7 @@ export const runtime = (): CommandRuntime => { if (!modalDialogVisible && (isInsideContainer(activeElement, 'codeMirrorEditor') || isInsideContainer(activeElement, 'tox-edit-area__iframe'))) { await CommandService.instance().execute('replaceSelection', suggestion); } else { - bridge().mainWindow().webContents.replaceMisspelling(suggestion); + bridge().activeWindow().webContents.replaceMisspelling(suggestion); } }, }; From 79098bdbabcf054206b71fbdf98a8503aca8c394 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Mon, 4 Nov 2024 06:53:54 -0800 Subject: [PATCH 83/97] Update yarn.lock --- yarn.lock | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/yarn.lock b/yarn.lock index 6fed1af0db9..1c34c01801f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13093,6 +13093,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:18.19.43": + version: 18.19.43 + resolution: "@types/node@npm:18.19.43" + dependencies: + undici-types: ~5.26.4 + checksum: 5eb9045aae6da86e8ad297381f93d29d2e7fcd4ed0c53670d9dff1e7b714920f8bbe5ee456289c19fc69c510ac197bdbacc7a785eaeba0afb9cb5d634a64bcd3 + languageName: node + linkType: hard + "@types/node@npm:^17.0.5": version: 17.0.45 resolution: "@types/node@npm:17.0.45" From 62f6b4a520bd8d2f2b251d604dbfffac0fd41320 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Mon, 4 Nov 2024 12:05:01 -0800 Subject: [PATCH 84/97] Throw if openNoteInNewWindow is called with no note selected --- packages/app-desktop/commands/openNoteInNewWindow.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/app-desktop/commands/openNoteInNewWindow.ts b/packages/app-desktop/commands/openNoteInNewWindow.ts index 0291eb3ee12..5ab595e9596 100644 --- a/packages/app-desktop/commands/openNoteInNewWindow.ts +++ b/packages/app-desktop/commands/openNoteInNewWindow.ts @@ -17,6 +17,10 @@ export const runtime = (): CommandRuntime => { return { execute: async (context: CommandContext, noteId: string = null) => { noteId = noteId || stateUtils.selectedNoteId(context.state); + if (!noteId) { + throw new Error('Error: No notes are selected. Select at least one note to open in a new window.'); + } + const note = await Note.load(noteId, { fields: ['parent_id'] }); context.dispatch({ type: 'WINDOW_OPEN', From 07dbd7a3915f37037655ab93be64c8f0fa6edfe4 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Mon, 4 Nov 2024 12:12:05 -0800 Subject: [PATCH 85/97] Rename WindowCommandHandler to WindowCommandsAndDialogs See https://github.com/laurent22/joplin/pull/11181#discussion_r1826209953 Note: The name might still be improvable --- .eslintignore | 116 +++++++++--------- .gitignore | 116 +++++++++--------- packages/app-desktop/app.ts | 2 +- .../gui/NoteEditor/EditorWindow.tsx | 4 +- packages/app-desktop/gui/Root.tsx | 4 +- .../AppDialogs.tsx | 0 .../ModalMessageOverlay.tsx | 0 .../PluginDialogs.tsx | 0 .../WindowCommandsAndDialogs.tsx} | 4 +- .../commands/addProfile.ts | 0 .../commands/commandPalette.ts | 0 .../commands/deleteFolder.ts | 0 .../commands/duplicateNote.ts | 0 .../commands/editAlarm.ts | 0 .../commands/exportPdf.ts | 0 .../commands/gotoAnything.ts | 0 .../commands/hideModalMessage.ts | 0 .../commands/index.ts | 0 .../commands/leaveSharedFolder.ts | 0 .../commands/moveToFolder.ts | 0 .../commands/newFolder.ts | 0 .../commands/newNote.ts | 0 .../commands/newSubFolder.ts | 0 .../commands/newTodo.ts | 0 .../commands/openFolder.ts | 0 .../commands/openFolderDialog.ts | 0 .../commands/openItem.ts | 0 .../commands/openNote.ts | 0 .../commands/openPdfViewer.ts | 0 .../commands/openTag.ts | 0 .../commands/print.ts | 0 .../commands/renameFolder.ts | 0 .../commands/renameTag.ts | 0 .../commands/resetLayout.ts | 0 .../commands/restoreFolder.ts | 0 .../commands/restoreNote.ts | 0 .../commands/revealResourceFile.ts | 0 .../commands/search.ts | 0 .../commands/setTags.ts | 0 .../commands/showModalMessage.ts | 0 .../commands/showNoteContentProperties.ts | 0 .../commands/showNoteProperties.ts | 0 .../commands/showPrompt.ts | 0 .../commands/showShareFolderDialog.ts | 0 .../commands/showShareNoteDialog.ts | 0 .../commands/showSpellCheckerMenu.test.ts | 0 .../commands/showSpellCheckerMenu.ts | 0 .../commands/toggleEditors.ts | 0 .../commands/toggleLayoutMoveMode.ts | 0 .../commands/toggleMenuBar.ts | 0 .../commands/toggleNoteList.ts | 0 .../commands/toggleNoteType.ts | 0 .../commands/toggleNotesSortOrderField.ts | 0 .../commands/toggleNotesSortOrderReverse.ts | 0 .../commands/togglePerFolderSortOrder.ts | 0 .../commands/toggleSideBar.ts | 0 .../commands/toggleVisiblePanes.ts | 0 .../types.ts | 0 .../utils/appDialogs.tsx | 0 .../utils/usePrintToCallback.ts | 0 .../utils/useSyncDialogState.ts | 0 .../utils/useWindowCommands.ts | 0 .../utils/useWindowControl.ts | 0 .../tools/gulp/tasks/buildScriptIndexes.js | 2 +- 64 files changed, 124 insertions(+), 124 deletions(-) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/AppDialogs.tsx (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/ModalMessageOverlay.tsx (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/PluginDialogs.tsx (100%) rename packages/app-desktop/gui/{WindowCommandHandler/WindowCommandHandler.tsx => WindowCommandsAndDialogs/WindowCommandsAndDialogs.tsx} (98%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/addProfile.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/commandPalette.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/deleteFolder.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/duplicateNote.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/editAlarm.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/exportPdf.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/gotoAnything.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/hideModalMessage.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/index.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/leaveSharedFolder.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/moveToFolder.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/newFolder.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/newNote.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/newSubFolder.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/newTodo.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/openFolder.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/openFolderDialog.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/openItem.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/openNote.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/openPdfViewer.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/openTag.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/print.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/renameFolder.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/renameTag.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/resetLayout.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/restoreFolder.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/restoreNote.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/revealResourceFile.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/search.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/setTags.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/showModalMessage.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/showNoteContentProperties.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/showNoteProperties.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/showPrompt.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/showShareFolderDialog.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/showShareNoteDialog.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/showSpellCheckerMenu.test.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/showSpellCheckerMenu.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/toggleEditors.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/toggleLayoutMoveMode.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/toggleMenuBar.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/toggleNoteList.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/toggleNoteType.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/toggleNotesSortOrderField.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/toggleNotesSortOrderReverse.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/togglePerFolderSortOrder.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/toggleSideBar.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/commands/toggleVisiblePanes.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/types.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/utils/appDialogs.tsx (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/utils/usePrintToCallback.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/utils/useSyncDialogState.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/utils/useWindowCommands.ts (100%) rename packages/app-desktop/gui/{WindowCommandHandler => WindowCommandsAndDialogs}/utils/useWindowControl.ts (100%) diff --git a/.eslintignore b/.eslintignore index 5d6e3d3333f..e765f0278d9 100644 --- a/.eslintignore +++ b/.eslintignore @@ -409,64 +409,64 @@ packages/app-desktop/gui/ToolbarButton/ToolbarButton.js packages/app-desktop/gui/ToolbarSpace.js packages/app-desktop/gui/TrashNotification/TrashNotification.js packages/app-desktop/gui/UpdateNotification/UpdateNotification.js -packages/app-desktop/gui/WindowCommandHandler/AppDialogs.js -packages/app-desktop/gui/WindowCommandHandler/ModalMessageOverlay.js -packages/app-desktop/gui/WindowCommandHandler/PluginDialogs.js -packages/app-desktop/gui/WindowCommandHandler/WindowCommandHandler.js -packages/app-desktop/gui/WindowCommandHandler/commands/addProfile.js -packages/app-desktop/gui/WindowCommandHandler/commands/commandPalette.js -packages/app-desktop/gui/WindowCommandHandler/commands/deleteFolder.js -packages/app-desktop/gui/WindowCommandHandler/commands/duplicateNote.js -packages/app-desktop/gui/WindowCommandHandler/commands/editAlarm.js -packages/app-desktop/gui/WindowCommandHandler/commands/exportPdf.js -packages/app-desktop/gui/WindowCommandHandler/commands/gotoAnything.js -packages/app-desktop/gui/WindowCommandHandler/commands/hideModalMessage.js -packages/app-desktop/gui/WindowCommandHandler/commands/index.js -packages/app-desktop/gui/WindowCommandHandler/commands/leaveSharedFolder.js -packages/app-desktop/gui/WindowCommandHandler/commands/moveToFolder.js -packages/app-desktop/gui/WindowCommandHandler/commands/newFolder.js -packages/app-desktop/gui/WindowCommandHandler/commands/newNote.js -packages/app-desktop/gui/WindowCommandHandler/commands/newSubFolder.js -packages/app-desktop/gui/WindowCommandHandler/commands/newTodo.js -packages/app-desktop/gui/WindowCommandHandler/commands/openFolder.js -packages/app-desktop/gui/WindowCommandHandler/commands/openFolderDialog.js -packages/app-desktop/gui/WindowCommandHandler/commands/openItem.js -packages/app-desktop/gui/WindowCommandHandler/commands/openNote.js -packages/app-desktop/gui/WindowCommandHandler/commands/openPdfViewer.js -packages/app-desktop/gui/WindowCommandHandler/commands/openTag.js -packages/app-desktop/gui/WindowCommandHandler/commands/print.js -packages/app-desktop/gui/WindowCommandHandler/commands/renameFolder.js -packages/app-desktop/gui/WindowCommandHandler/commands/renameTag.js -packages/app-desktop/gui/WindowCommandHandler/commands/resetLayout.js -packages/app-desktop/gui/WindowCommandHandler/commands/restoreFolder.js -packages/app-desktop/gui/WindowCommandHandler/commands/restoreNote.js -packages/app-desktop/gui/WindowCommandHandler/commands/revealResourceFile.js -packages/app-desktop/gui/WindowCommandHandler/commands/search.js -packages/app-desktop/gui/WindowCommandHandler/commands/setTags.js -packages/app-desktop/gui/WindowCommandHandler/commands/showModalMessage.js -packages/app-desktop/gui/WindowCommandHandler/commands/showNoteContentProperties.js -packages/app-desktop/gui/WindowCommandHandler/commands/showNoteProperties.js -packages/app-desktop/gui/WindowCommandHandler/commands/showPrompt.js -packages/app-desktop/gui/WindowCommandHandler/commands/showShareFolderDialog.js -packages/app-desktop/gui/WindowCommandHandler/commands/showShareNoteDialog.js -packages/app-desktop/gui/WindowCommandHandler/commands/showSpellCheckerMenu.test.js -packages/app-desktop/gui/WindowCommandHandler/commands/showSpellCheckerMenu.js -packages/app-desktop/gui/WindowCommandHandler/commands/toggleEditors.js -packages/app-desktop/gui/WindowCommandHandler/commands/toggleLayoutMoveMode.js -packages/app-desktop/gui/WindowCommandHandler/commands/toggleMenuBar.js -packages/app-desktop/gui/WindowCommandHandler/commands/toggleNoteList.js -packages/app-desktop/gui/WindowCommandHandler/commands/toggleNoteType.js -packages/app-desktop/gui/WindowCommandHandler/commands/toggleNotesSortOrderField.js -packages/app-desktop/gui/WindowCommandHandler/commands/toggleNotesSortOrderReverse.js -packages/app-desktop/gui/WindowCommandHandler/commands/togglePerFolderSortOrder.js -packages/app-desktop/gui/WindowCommandHandler/commands/toggleSideBar.js -packages/app-desktop/gui/WindowCommandHandler/commands/toggleVisiblePanes.js -packages/app-desktop/gui/WindowCommandHandler/types.js -packages/app-desktop/gui/WindowCommandHandler/utils/appDialogs.js -packages/app-desktop/gui/WindowCommandHandler/utils/usePrintToCallback.js -packages/app-desktop/gui/WindowCommandHandler/utils/useSyncDialogState.js -packages/app-desktop/gui/WindowCommandHandler/utils/useWindowCommands.js -packages/app-desktop/gui/WindowCommandHandler/utils/useWindowControl.js +packages/app-desktop/gui/WindowCommandsAndDialogs/AppDialogs.js +packages/app-desktop/gui/WindowCommandsAndDialogs/ModalMessageOverlay.js +packages/app-desktop/gui/WindowCommandsAndDialogs/PluginDialogs.js +packages/app-desktop/gui/WindowCommandsAndDialogs/WindowCommandsAndDialogs.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/addProfile.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/commandPalette.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/deleteFolder.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/duplicateNote.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/editAlarm.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/exportPdf.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/leaveSharedFolder.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newFolder.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newNote.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newSubFolder.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newTodo.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openFolder.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openFolderDialog.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openItem.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openNote.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openPdfViewer.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openTag.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/print.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/renameFolder.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/renameTag.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/resetLayout.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreFolder.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreNote.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/revealResourceFile.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/search.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/setTags.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showModalMessage.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteContentProperties.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteProperties.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showPrompt.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareFolderDialog.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareNoteDialog.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.test.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleEditors.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleLayoutMoveMode.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleMenuBar.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNoteList.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNoteType.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNotesSortOrderField.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNotesSortOrderReverse.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/togglePerFolderSortOrder.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleSideBar.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleVisiblePanes.js +packages/app-desktop/gui/WindowCommandsAndDialogs/types.js +packages/app-desktop/gui/WindowCommandsAndDialogs/utils/appDialogs.js +packages/app-desktop/gui/WindowCommandsAndDialogs/utils/usePrintToCallback.js +packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useSyncDialogState.js +packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowCommands.js +packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowControl.js packages/app-desktop/gui/dialogs.js packages/app-desktop/gui/hooks/useDom.js packages/app-desktop/gui/hooks/useEffectDebugger.js diff --git a/.gitignore b/.gitignore index 1eedede4161..e4cb418b3f6 100644 --- a/.gitignore +++ b/.gitignore @@ -386,64 +386,64 @@ packages/app-desktop/gui/ToolbarButton/ToolbarButton.js packages/app-desktop/gui/ToolbarSpace.js packages/app-desktop/gui/TrashNotification/TrashNotification.js packages/app-desktop/gui/UpdateNotification/UpdateNotification.js -packages/app-desktop/gui/WindowCommandHandler/AppDialogs.js -packages/app-desktop/gui/WindowCommandHandler/ModalMessageOverlay.js -packages/app-desktop/gui/WindowCommandHandler/PluginDialogs.js -packages/app-desktop/gui/WindowCommandHandler/WindowCommandHandler.js -packages/app-desktop/gui/WindowCommandHandler/commands/addProfile.js -packages/app-desktop/gui/WindowCommandHandler/commands/commandPalette.js -packages/app-desktop/gui/WindowCommandHandler/commands/deleteFolder.js -packages/app-desktop/gui/WindowCommandHandler/commands/duplicateNote.js -packages/app-desktop/gui/WindowCommandHandler/commands/editAlarm.js -packages/app-desktop/gui/WindowCommandHandler/commands/exportPdf.js -packages/app-desktop/gui/WindowCommandHandler/commands/gotoAnything.js -packages/app-desktop/gui/WindowCommandHandler/commands/hideModalMessage.js -packages/app-desktop/gui/WindowCommandHandler/commands/index.js -packages/app-desktop/gui/WindowCommandHandler/commands/leaveSharedFolder.js -packages/app-desktop/gui/WindowCommandHandler/commands/moveToFolder.js -packages/app-desktop/gui/WindowCommandHandler/commands/newFolder.js -packages/app-desktop/gui/WindowCommandHandler/commands/newNote.js -packages/app-desktop/gui/WindowCommandHandler/commands/newSubFolder.js -packages/app-desktop/gui/WindowCommandHandler/commands/newTodo.js -packages/app-desktop/gui/WindowCommandHandler/commands/openFolder.js -packages/app-desktop/gui/WindowCommandHandler/commands/openFolderDialog.js -packages/app-desktop/gui/WindowCommandHandler/commands/openItem.js -packages/app-desktop/gui/WindowCommandHandler/commands/openNote.js -packages/app-desktop/gui/WindowCommandHandler/commands/openPdfViewer.js -packages/app-desktop/gui/WindowCommandHandler/commands/openTag.js -packages/app-desktop/gui/WindowCommandHandler/commands/print.js -packages/app-desktop/gui/WindowCommandHandler/commands/renameFolder.js -packages/app-desktop/gui/WindowCommandHandler/commands/renameTag.js -packages/app-desktop/gui/WindowCommandHandler/commands/resetLayout.js -packages/app-desktop/gui/WindowCommandHandler/commands/restoreFolder.js -packages/app-desktop/gui/WindowCommandHandler/commands/restoreNote.js -packages/app-desktop/gui/WindowCommandHandler/commands/revealResourceFile.js -packages/app-desktop/gui/WindowCommandHandler/commands/search.js -packages/app-desktop/gui/WindowCommandHandler/commands/setTags.js -packages/app-desktop/gui/WindowCommandHandler/commands/showModalMessage.js -packages/app-desktop/gui/WindowCommandHandler/commands/showNoteContentProperties.js -packages/app-desktop/gui/WindowCommandHandler/commands/showNoteProperties.js -packages/app-desktop/gui/WindowCommandHandler/commands/showPrompt.js -packages/app-desktop/gui/WindowCommandHandler/commands/showShareFolderDialog.js -packages/app-desktop/gui/WindowCommandHandler/commands/showShareNoteDialog.js -packages/app-desktop/gui/WindowCommandHandler/commands/showSpellCheckerMenu.test.js -packages/app-desktop/gui/WindowCommandHandler/commands/showSpellCheckerMenu.js -packages/app-desktop/gui/WindowCommandHandler/commands/toggleEditors.js -packages/app-desktop/gui/WindowCommandHandler/commands/toggleLayoutMoveMode.js -packages/app-desktop/gui/WindowCommandHandler/commands/toggleMenuBar.js -packages/app-desktop/gui/WindowCommandHandler/commands/toggleNoteList.js -packages/app-desktop/gui/WindowCommandHandler/commands/toggleNoteType.js -packages/app-desktop/gui/WindowCommandHandler/commands/toggleNotesSortOrderField.js -packages/app-desktop/gui/WindowCommandHandler/commands/toggleNotesSortOrderReverse.js -packages/app-desktop/gui/WindowCommandHandler/commands/togglePerFolderSortOrder.js -packages/app-desktop/gui/WindowCommandHandler/commands/toggleSideBar.js -packages/app-desktop/gui/WindowCommandHandler/commands/toggleVisiblePanes.js -packages/app-desktop/gui/WindowCommandHandler/types.js -packages/app-desktop/gui/WindowCommandHandler/utils/appDialogs.js -packages/app-desktop/gui/WindowCommandHandler/utils/usePrintToCallback.js -packages/app-desktop/gui/WindowCommandHandler/utils/useSyncDialogState.js -packages/app-desktop/gui/WindowCommandHandler/utils/useWindowCommands.js -packages/app-desktop/gui/WindowCommandHandler/utils/useWindowControl.js +packages/app-desktop/gui/WindowCommandsAndDialogs/AppDialogs.js +packages/app-desktop/gui/WindowCommandsAndDialogs/ModalMessageOverlay.js +packages/app-desktop/gui/WindowCommandsAndDialogs/PluginDialogs.js +packages/app-desktop/gui/WindowCommandsAndDialogs/WindowCommandsAndDialogs.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/addProfile.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/commandPalette.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/deleteFolder.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/duplicateNote.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/editAlarm.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/exportPdf.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/leaveSharedFolder.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newFolder.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newNote.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newSubFolder.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newTodo.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openFolder.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openFolderDialog.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openItem.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openNote.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openPdfViewer.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openTag.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/print.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/renameFolder.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/renameTag.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/resetLayout.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreFolder.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreNote.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/revealResourceFile.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/search.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/setTags.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showModalMessage.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteContentProperties.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteProperties.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showPrompt.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareFolderDialog.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareNoteDialog.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.test.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleEditors.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleLayoutMoveMode.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleMenuBar.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNoteList.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNoteType.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNotesSortOrderField.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNotesSortOrderReverse.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/togglePerFolderSortOrder.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleSideBar.js +packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleVisiblePanes.js +packages/app-desktop/gui/WindowCommandsAndDialogs/types.js +packages/app-desktop/gui/WindowCommandsAndDialogs/utils/appDialogs.js +packages/app-desktop/gui/WindowCommandsAndDialogs/utils/usePrintToCallback.js +packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useSyncDialogState.js +packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowCommands.js +packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowControl.js packages/app-desktop/gui/dialogs.js packages/app-desktop/gui/hooks/useDom.js packages/app-desktop/gui/hooks/useEffectDebugger.js diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index d8b34e88b10..0d3ecbeeced 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -35,7 +35,7 @@ const PluginManager = require('@joplin/lib/services/PluginManager'); import RevisionService from '@joplin/lib/services/RevisionService'; import MigrationService from '@joplin/lib/services/MigrationService'; import { loadCustomCss } from '@joplin/lib/CssUtils'; -import mainScreenCommands from './gui/WindowCommandHandler/commands/index'; +import mainScreenCommands from './gui/WindowCommandsAndDialogs/commands/index'; import noteEditorCommands from './gui/NoteEditor/commands/index'; import noteListCommands from './gui/NoteList/commands/index'; import noteListControlsCommands from './gui/NoteListControls/commands/index'; diff --git a/packages/app-desktop/gui/NoteEditor/EditorWindow.tsx b/packages/app-desktop/gui/NoteEditor/EditorWindow.tsx index 93c1f4e0cce..4443b26fc0d 100644 --- a/packages/app-desktop/gui/NoteEditor/EditorWindow.tsx +++ b/packages/app-desktop/gui/NoteEditor/EditorWindow.tsx @@ -6,7 +6,7 @@ import { connect } from 'react-redux'; import { AppState } from '../../app.reducer'; import { Dispatch } from 'redux'; import NewWindowOrIFrame, { WindowMode } from '../NewWindowOrIFrame'; -import WindowCommandHandler from '../WindowCommandHandler/WindowCommandHandler'; +import WindowCommandsAndDialogs from '../WindowCommandsAndDialogs/WindowCommandsAndDialogs'; const { StyleSheetManager } = require('styled-components'); // Note: Transitive dependencies used only by react-select. Remove if react-select is removed. @@ -74,7 +74,7 @@ const SecondaryWindow: React.FC = props => { title={windowTitle} > - + {editor} diff --git a/packages/app-desktop/gui/Root.tsx b/packages/app-desktop/gui/Root.tsx index 212dd27985a..ef0d5727760 100644 --- a/packages/app-desktop/gui/Root.tsx +++ b/packages/app-desktop/gui/Root.tsx @@ -25,7 +25,7 @@ const { ResourceScreen } = require('./ResourceScreen.js'); import Navigator from './Navigator'; import WelcomeUtils from '@joplin/lib/WelcomeUtils'; import JoplinCloudLoginScreen from './JoplinCloudLoginScreen'; -import WindowCommandHandler from './WindowCommandHandler/WindowCommandHandler'; +import WindowCommandsAndDialogs from './WindowCommandsAndDialogs/WindowCommandsAndDialogs'; import { defaultWindowId, stateUtils, WindowState } from '@joplin/lib/reducer'; import bridge from '../services/bridge'; import EditorWindow from './NoteEditor/EditorWindow'; @@ -196,7 +196,7 @@ class RootComponent extends React.Component { - + {this.renderSecondaryWindows()} {this.renderModalMessage(this.modalDialogProps())} diff --git a/packages/app-desktop/gui/WindowCommandHandler/AppDialogs.tsx b/packages/app-desktop/gui/WindowCommandsAndDialogs/AppDialogs.tsx similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/AppDialogs.tsx rename to packages/app-desktop/gui/WindowCommandsAndDialogs/AppDialogs.tsx diff --git a/packages/app-desktop/gui/WindowCommandHandler/ModalMessageOverlay.tsx b/packages/app-desktop/gui/WindowCommandsAndDialogs/ModalMessageOverlay.tsx similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/ModalMessageOverlay.tsx rename to packages/app-desktop/gui/WindowCommandsAndDialogs/ModalMessageOverlay.tsx diff --git a/packages/app-desktop/gui/WindowCommandHandler/PluginDialogs.tsx b/packages/app-desktop/gui/WindowCommandsAndDialogs/PluginDialogs.tsx similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/PluginDialogs.tsx rename to packages/app-desktop/gui/WindowCommandsAndDialogs/PluginDialogs.tsx diff --git a/packages/app-desktop/gui/WindowCommandHandler/WindowCommandHandler.tsx b/packages/app-desktop/gui/WindowCommandsAndDialogs/WindowCommandsAndDialogs.tsx similarity index 98% rename from packages/app-desktop/gui/WindowCommandHandler/WindowCommandHandler.tsx rename to packages/app-desktop/gui/WindowCommandsAndDialogs/WindowCommandsAndDialogs.tsx index 6af867dc325..e97ace2ce02 100644 --- a/packages/app-desktop/gui/WindowCommandHandler/WindowCommandHandler.tsx +++ b/packages/app-desktop/gui/WindowCommandsAndDialogs/WindowCommandsAndDialogs.tsx @@ -71,7 +71,7 @@ const useSyncActiveWindow = (containerWindow: Window|null) => { }, [containerWindow]); }; -const WindowCommandHandler: React.FC = props => { +const WindowCommandsAndDialogs: React.FC = props => { const [referenceElement, setReferenceElement] = useState(null); const containerDocument = useDom(referenceElement); @@ -194,4 +194,4 @@ export default connect((state: AppState, ownProps: ConnectProps) => { pluginsLegacy: state.pluginsLegacy, modalMessage: state.modalOverlayMessage, }; -})(WindowCommandHandler); +})(WindowCommandsAndDialogs); diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/addProfile.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/addProfile.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/addProfile.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/addProfile.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/commandPalette.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/commandPalette.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/commandPalette.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/commandPalette.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/deleteFolder.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/deleteFolder.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/deleteFolder.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/deleteFolder.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/duplicateNote.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/duplicateNote.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/duplicateNote.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/duplicateNote.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/editAlarm.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/editAlarm.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/editAlarm.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/editAlarm.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/exportPdf.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/exportPdf.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/exportPdf.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/exportPdf.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/gotoAnything.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/gotoAnything.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/hideModalMessage.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/hideModalMessage.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/index.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/index.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/leaveSharedFolder.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/leaveSharedFolder.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/leaveSharedFolder.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/leaveSharedFolder.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/moveToFolder.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/moveToFolder.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/newFolder.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newFolder.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/newFolder.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newFolder.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/newNote.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newNote.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/newNote.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newNote.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/newSubFolder.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newSubFolder.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/newSubFolder.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newSubFolder.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/newTodo.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newTodo.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/newTodo.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newTodo.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/openFolder.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openFolder.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/openFolder.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openFolder.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/openFolderDialog.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openFolderDialog.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/openFolderDialog.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openFolderDialog.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/openItem.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openItem.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/openItem.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openItem.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/openNote.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openNote.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/openNote.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openNote.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/openPdfViewer.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openPdfViewer.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/openPdfViewer.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openPdfViewer.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/openTag.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openTag.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/openTag.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/openTag.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/print.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/print.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/print.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/print.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/renameFolder.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/renameFolder.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/renameFolder.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/renameFolder.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/renameTag.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/renameTag.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/renameTag.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/renameTag.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/resetLayout.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/resetLayout.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/resetLayout.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/resetLayout.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/restoreFolder.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreFolder.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/restoreFolder.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreFolder.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/restoreNote.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreNote.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/restoreNote.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/restoreNote.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/revealResourceFile.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/revealResourceFile.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/revealResourceFile.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/revealResourceFile.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/search.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/search.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/search.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/search.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/setTags.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/setTags.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/setTags.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/setTags.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/showModalMessage.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showModalMessage.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/showModalMessage.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showModalMessage.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/showNoteContentProperties.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteContentProperties.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/showNoteContentProperties.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteContentProperties.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/showNoteProperties.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteProperties.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/showNoteProperties.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showNoteProperties.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/showPrompt.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showPrompt.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/showPrompt.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showPrompt.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/showShareFolderDialog.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareFolderDialog.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/showShareFolderDialog.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareFolderDialog.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/showShareNoteDialog.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareNoteDialog.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/showShareNoteDialog.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showShareNoteDialog.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/showSpellCheckerMenu.test.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.test.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/showSpellCheckerMenu.test.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.test.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/showSpellCheckerMenu.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/showSpellCheckerMenu.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/toggleEditors.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleEditors.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/toggleEditors.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleEditors.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/toggleLayoutMoveMode.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleLayoutMoveMode.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/toggleLayoutMoveMode.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleLayoutMoveMode.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/toggleMenuBar.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleMenuBar.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/toggleMenuBar.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleMenuBar.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/toggleNoteList.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNoteList.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/toggleNoteList.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNoteList.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/toggleNoteType.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNoteType.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/toggleNoteType.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNoteType.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/toggleNotesSortOrderField.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNotesSortOrderField.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/toggleNotesSortOrderField.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNotesSortOrderField.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/toggleNotesSortOrderReverse.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNotesSortOrderReverse.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/toggleNotesSortOrderReverse.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleNotesSortOrderReverse.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/togglePerFolderSortOrder.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/togglePerFolderSortOrder.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/togglePerFolderSortOrder.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/togglePerFolderSortOrder.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/toggleSideBar.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleSideBar.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/toggleSideBar.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleSideBar.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/commands/toggleVisiblePanes.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleVisiblePanes.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/commands/toggleVisiblePanes.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/commands/toggleVisiblePanes.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/types.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/types.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/types.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/types.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/utils/appDialogs.tsx b/packages/app-desktop/gui/WindowCommandsAndDialogs/utils/appDialogs.tsx similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/utils/appDialogs.tsx rename to packages/app-desktop/gui/WindowCommandsAndDialogs/utils/appDialogs.tsx diff --git a/packages/app-desktop/gui/WindowCommandHandler/utils/usePrintToCallback.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/utils/usePrintToCallback.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/utils/usePrintToCallback.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/utils/usePrintToCallback.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/utils/useSyncDialogState.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useSyncDialogState.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/utils/useSyncDialogState.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useSyncDialogState.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/utils/useWindowCommands.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowCommands.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/utils/useWindowCommands.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowCommands.ts diff --git a/packages/app-desktop/gui/WindowCommandHandler/utils/useWindowControl.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowControl.ts similarity index 100% rename from packages/app-desktop/gui/WindowCommandHandler/utils/useWindowControl.ts rename to packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowControl.ts diff --git a/packages/tools/gulp/tasks/buildScriptIndexes.js b/packages/tools/gulp/tasks/buildScriptIndexes.js index 94dea9ef80b..f746d931e4c 100644 --- a/packages/tools/gulp/tasks/buildScriptIndexes.js +++ b/packages/tools/gulp/tasks/buildScriptIndexes.js @@ -62,7 +62,7 @@ module.exports = { src: '', fn: async function() { await processDirectory(`${rootDir}/packages/app-desktop/commands`); - await processDirectory(`${rootDir}/packages/app-desktop/gui/WindowCommandHandler/commands`); + await processDirectory(`${rootDir}/packages/app-desktop/gui/WindowCommandsAndDialogs/commands`); await processDirectory(`${rootDir}/packages/app-desktop/gui/NoteEditor/commands`); await processDirectory(`${rootDir}/packages/app-desktop/gui/NoteList/commands`); await processDirectory(`${rootDir}/packages/app-desktop/gui/NoteListControls/commands`); From d11074b4a60b20345150e734bb751499a0ad74ab Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Mon, 4 Nov 2024 12:48:33 -0800 Subject: [PATCH 86/97] Fix clicking on links in the Rich Text editor opens two copies of the link in new windows This commit ensures that the global event handlers added in main-html.js are also added in new windows. --- .../app-desktop/gui/NewWindowOrIFrame.tsx | 1 + packages/app-desktop/index.html | 1 + packages/app-desktop/main-html.js | 21 ----------------- .../utils/window/eventHandlerOverrides.js | 23 +++++++++++++++++++ 4 files changed, 25 insertions(+), 21 deletions(-) create mode 100644 packages/app-desktop/utils/window/eventHandlerOverrides.js diff --git a/packages/app-desktop/gui/NewWindowOrIFrame.tsx b/packages/app-desktop/gui/NewWindowOrIFrame.tsx index e27028ad71d..d8781233176 100644 --- a/packages/app-desktop/gui/NewWindowOrIFrame.tsx +++ b/packages/app-desktop/gui/NewWindowOrIFrame.tsx @@ -116,6 +116,7 @@ const useDocumentSetup = (doc: Document|null, setLoaded: OnSetLoaded, onFocus?: const jsUrls = [ 'vendor/lib/smalltalk/dist/smalltalk.min.js', + './utils/window/eventHandlerOverrides.js', ]; for (const url of jsUrls) { const script = doc.createElement('script'); diff --git a/packages/app-desktop/index.html b/packages/app-desktop/index.html index 4110c493839..797918d61bb 100644 --- a/packages/app-desktop/index.html +++ b/packages/app-desktop/index.html @@ -23,6 +23,7 @@
    + diff --git a/packages/app-desktop/main-html.js b/packages/app-desktop/main-html.js index 734d4600e3d..982cc7e02a2 100644 --- a/packages/app-desktop/main-html.js +++ b/packages/app-desktop/main-html.js @@ -112,27 +112,6 @@ const main = async () => { pdfJs, }); - // Disable drag and drop of links inside application (which would - // open it as if the whole app was a browser) - document.addEventListener('dragover', event => event.preventDefault()); - document.addEventListener('drop', event => event.preventDefault()); - - // Disable middle-click (which would open a new browser window, but we don't want this) - document.addEventListener('auxclick', event => event.preventDefault()); - - // Each link (rendered as a button or list item) has its own custom click event - // so disable the default. In particular this will disable Ctrl+Clicking a link - // which would open a new browser window. - document.addEventListener('click', (event) => { - // We don't apply this to labels and inputs because it would break - // checkboxes. Such a global event handler is probably not a good idea - // anyway but keeping it for now, as it doesn't seem to break anything else. - // https://github.com/facebook/react/issues/13477#issuecomment-489274045 - if (['LABEL', 'INPUT'].includes(event.target.nodeName)) return; - - event.preventDefault(); - }); - const logger = new Logger(); Logger.initializeGlobalLogger(logger); initLib(logger); diff --git a/packages/app-desktop/utils/window/eventHandlerOverrides.js b/packages/app-desktop/utils/window/eventHandlerOverrides.js new file mode 100644 index 00000000000..ac12336fecd --- /dev/null +++ b/packages/app-desktop/utils/window/eventHandlerOverrides.js @@ -0,0 +1,23 @@ +// TODO: These are already partially replaced by Electron main-process +// event handlers. In the future, these may be possible to remove. + +// Disable drag and drop of links inside application (which would +// open it as if the whole app was a browser) +document.addEventListener('dragover', event => event.preventDefault()); +document.addEventListener('drop', event => event.preventDefault()); + +// Disable middle-click (which would open a new browser window, but we don't want this) +document.addEventListener('auxclick', event => event.preventDefault()); + +// Each link (rendered as a button or list item) has its own custom click event +// so disable the default. In particular this will disable Ctrl+Clicking a link +// which would open a new browser window. +document.addEventListener('click', (event) => { + // We don't apply this to labels and inputs because it would break + // checkboxes. Such a global event handler is probably not a good idea + // anyway but keeping it for now, as it doesn't seem to break anything else. + // https://github.com/facebook/react/issues/13477#issuecomment-489274045 + if (['LABEL', 'INPUT'].includes(event.target.nodeName)) return; + + event.preventDefault(); +}); From a72e4e34ef9d0d6dcb0d5089816c62597b579e06 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Mon, 4 Nov 2024 12:57:16 -0800 Subject: [PATCH 87/97] Fix errors post merge --- packages/app-desktop/gui/NoteTextViewer.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/app-desktop/gui/NoteTextViewer.tsx b/packages/app-desktop/gui/NoteTextViewer.tsx index 34bfa49967a..78c428a8fb5 100644 --- a/packages/app-desktop/gui/NoteTextViewer.tsx +++ b/packages/app-desktop/gui/NoteTextViewer.tsx @@ -198,8 +198,14 @@ const NoteTextViewer = forwardRef((props: Props, ref: ForwardedRef Date: Mon, 4 Nov 2024 12:58:59 -0800 Subject: [PATCH 88/97] Trying to fix flaky test --- packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.ts b/packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.ts index b792bada384..68b9cef3fa3 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.ts @@ -75,7 +75,8 @@ describe('useFormNote', () => { expect(formNote.result.current.formNote).toMatchObject({ encryption_applied: 0, }); - }); + // A larger-than-default timeout is needed to prevent CI failures: + }, { timeout: 5_000 }); formNote.unmount(); }); From f2757f9a08193667c7ca8a353d1fbc543fe2d58d Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Tue, 5 Nov 2024 09:09:35 -0800 Subject: [PATCH 89/97] Update background_windows.md with more information about React portals --- readme/dev/spec/background_windows.md | 54 +++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/readme/dev/spec/background_windows.md b/readme/dev/spec/background_windows.md index e0d1f96fc3d..177eae29f0c 100644 --- a/readme/dev/spec/background_windows.md +++ b/readme/dev/spec/background_windows.md @@ -1,8 +1,12 @@ # Background windows +This document explains Joplin's multiple window support from a technical perspective. + +## Reducer state + On desktop, Joplin supports multiple windows with the `backgroundWindows` reducer state. Each entry in `backgroundWindows` contains the state of a window that is in the background (not currently focused). The currently focused window's state is stored in the top-level state object. -## State structure +### State structure Suppose that there are three windows, `window1`, `window2`, and `window3`. @@ -40,7 +44,7 @@ state = { Notice that the state for the focused window is part of the main state object. This is for compatibility. Historically, Joplin only supported one window with at most one open editor. As a result, many commands assume that if `state.selectedNoteIds = ['some-id-here']`, then `state.selectedNoteIds[0]` is the ID of the note currently being edited. By storing the `selectedNoteIds` for the currently active window in the main `state` object, Joplin remains compatible with legacy commands and components. -## Switching windows +### Switching windows When the user switches from one window to another, a `WINDOW_FOCUS` action is sent to the reducer. The reducer responds by moving the old window's state to `backgroundWindows` and the new window's state to the main `state` object. @@ -93,7 +97,7 @@ For example, if a user switches from `window1` to `window2`, the state change lo ``` Window 2 is now the main window. -## Extra per-window state +### Extra per-window state Some per-window state is application specific. For example, the `devToolsVisible` property is **specific to the desktop app** and is different for each window (because each window can have its own dev tools). @@ -101,7 +105,49 @@ This is supported by giving `backgroundWindows` a type with an extended set of p When new windows are opened with the `WINDOW_OPEN` action, the default value for the new window-specific properties must be provided. For example, `{ devToolsVisible: false }`. -## Components: Getting the ID for the current window +## Rendering components in new windows + +The [react-dom portals API](https://react.dev/reference/react-dom/createPortal) allows a component to render its children in a different place. In this case, Joplin uses the portals API to render certain components in a new window, while keeping them in the same React component tree. + +### Same React tree, different DOMs + +The React portals API allows all editors to be descendants of the `` component, while also being in a different HTML `document`: + +```mermaid +flowchart TD + subgraph Main Window + Root + Root-->Navigator + Navigator-->MainContent("[[Main app content]]") + Root-->EditorWindow1 + Root-->EditorWindow2 + + EditorWindow1-->NewWindowOrIframe1-->Portal1(("Portal")) + EditorWindow2-->NewWindowOrIframe2-->Portal2(("Portal")) + end + + subgraph Window1 + NoteEditor1["Note editor"] + end + + subgraph Window2 + NoteEditor2["Note editor"] + end + + Portal1-->NoteEditor1 + Portal2-->NoteEditor2 +``` + +This also means that components running in secondary windows (e.g. `Window1`, `Window2` above) are controlled by JavaScript in the main window. As a result, +- Closing the main window means that secondary windows must also be closed. + - Hiding the window with `BrowserWindow.hide` is used instead. +- Component logic must be careful to reference the correct DOM. **Using the global `document` and `window` objects is often incorrect.** + - Instead of the `document` global variable, use [`HTMLElement.ownerDocument`](https://developer.mozilla.org/en-US/docs/Web/API/Node/ownerDocument) (or the `useDom` hook). + - Instead of `window`, use [`.defaultView`](https://developer.mozilla.org/en-US/docs/Web/API/Document/defaultView) on a [`Document`](https://developer.mozilla.org/en-US/docs/Web/API/Document) object. + +### Getting the ID for the current window + +Sometimes, it's necessary to get the Joplin ID of the current window. This ID allows referencing the state specific to a window in Joplin's reducer. Suppose `` could be displayed in any window of the app and we want to find the selected note IDs for its window. To do this: 1. Get the window ID. This can be done by either: From 6cae8889c0cfe90cde86923d4336357ce186d9fa Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Tue, 5 Nov 2024 09:16:29 -0800 Subject: [PATCH 90/97] Add a limitations section to background_windows.md --- readme/dev/spec/background_windows.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/readme/dev/spec/background_windows.md b/readme/dev/spec/background_windows.md index 177eae29f0c..9c0b8d3e612 100644 --- a/readme/dev/spec/background_windows.md +++ b/readme/dev/spec/background_windows.md @@ -155,3 +155,9 @@ Suppose `` could be displayed in any window of the app and we want - With `useContext(WindowIdContext)`, where `WindowIdContext` is the context that provides the window ID. ([Documentation for `useContext`](https://react.dev/reference/react/useContext)). 2. Use the window ID to determine the current window state. To do this, use the window ID to look up the state for the current window (e.g. with `stateUtils.windowStateById`). 3. Get the `selectedNoteIds` from the window state. + +## Limitations + +- **All windows have the same menubar:** Currently, Joplin sets the application menubar with `Menu.setApplicationMenu`. This allows the menu to be updated later with `Menu.getApplicationMenu`, but means that all windows have the same menu. + - An alternative might be to set menus on individual windows with [`BrowserWindow.setMenu`](https://www.electronjs.org/docs/latest/api/browser-window#winsetmenumenu-linux-windows), but this doesn't allow accessing the menu later (e.g. from Playwright) with `Menu.getApplicationMenu` and does not work on MacOS. +- **Certain UI only available on the main window:** At present, certain parts of the UI (e.g. sidebars, warning banners, etc.) are only visible on the main window.While this simplifies multi-window support by limiting which components need to work in secondary windows, it makes secondary windows more limited than primary windows. From ef4b9a2cfcfd7d745b36d8a50a031bbddaf31c7a Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Tue, 5 Nov 2024 09:33:54 -0800 Subject: [PATCH 91/97] Rename useDom -> useDocument --- .eslintignore | 2 +- .gitignore | 2 +- packages/app-desktop/gui/Dialog.tsx | 4 ++-- .../gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx | 4 ++-- packages/app-desktop/gui/NoteTextViewer.tsx | 4 ++-- .../gui/StyleSheets/StyleSheetContainer.tsx | 4 ++-- .../WindowCommandsAndDialogs.tsx | 4 ++-- packages/app-desktop/gui/hooks/useDocument.ts | 12 ++++++++++++ packages/app-desktop/gui/hooks/useDom.ts | 9 --------- 9 files changed, 24 insertions(+), 21 deletions(-) create mode 100644 packages/app-desktop/gui/hooks/useDocument.ts delete mode 100644 packages/app-desktop/gui/hooks/useDom.ts diff --git a/.eslintignore b/.eslintignore index e765f0278d9..bec45388aeb 100644 --- a/.eslintignore +++ b/.eslintignore @@ -468,7 +468,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useSyncDialogState.js packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowCommands.js packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowControl.js packages/app-desktop/gui/dialogs.js -packages/app-desktop/gui/hooks/useDom.js +packages/app-desktop/gui/hooks/useDocument.js packages/app-desktop/gui/hooks/useEffectDebugger.js packages/app-desktop/gui/hooks/useElementHeight.js packages/app-desktop/gui/hooks/useImperativeHandlerDebugger.js diff --git a/.gitignore b/.gitignore index e4cb418b3f6..8e17b5591c7 100644 --- a/.gitignore +++ b/.gitignore @@ -445,7 +445,7 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useSyncDialogState.js packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowCommands.js packages/app-desktop/gui/WindowCommandsAndDialogs/utils/useWindowControl.js packages/app-desktop/gui/dialogs.js -packages/app-desktop/gui/hooks/useDom.js +packages/app-desktop/gui/hooks/useDocument.js packages/app-desktop/gui/hooks/useEffectDebugger.js packages/app-desktop/gui/hooks/useElementHeight.js packages/app-desktop/gui/hooks/useImperativeHandlerDebugger.js diff --git a/packages/app-desktop/gui/Dialog.tsx b/packages/app-desktop/gui/Dialog.tsx index 48d83e21a0b..ce21f10f6b5 100644 --- a/packages/app-desktop/gui/Dialog.tsx +++ b/packages/app-desktop/gui/Dialog.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { ReactNode, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { blur, focus } from '@joplin/lib/utils/focusHandler'; -import useDom from './hooks/useDom'; +import useDocument from './hooks/useDocument'; type OnCancelListener = ()=> void; @@ -16,7 +16,7 @@ interface Props { const Dialog: React.FC = props => { const [containerElement, setContainerElement] = useState(null); - const containerDocument = useDom(containerElement); + const containerDocument = useDocument(containerElement); // For correct focus handling, the dialog element needs to be managed separately from React. In particular, // just after creating the dialog, we need to call .showModal() and just **before** closing the dialog, we diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx index 173a35ef9e9..cc4460fe19f 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx @@ -41,7 +41,7 @@ const supportedLocales = require('./supportedLocales'); import { hasProtocol } from '@joplin/utils/url'; import useTabIndenter from './utils/useTabIndenter'; import useKeyboardRefocusHandler from './utils/useKeyboardRefocusHandler'; -import useDom from '../../../hooks/useDom'; +import useDocument from '../../../hooks/useDocument'; const logger = Logger.create('TinyMCE'); @@ -101,7 +101,7 @@ let changeId_ = 1; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { const [editorContainer, setEditorContainer] = useState(null); - const editorContainerDom = useDom(editorContainer); + const editorContainerDom = useDocument(editorContainer); const [editor, setEditor] = useState(null); const [scriptLoaded, setScriptLoaded] = useState(false); const [editorReady, setEditorReady] = useState(false); diff --git a/packages/app-desktop/gui/NoteTextViewer.tsx b/packages/app-desktop/gui/NoteTextViewer.tsx index 78c428a8fb5..c34f534ff6e 100644 --- a/packages/app-desktop/gui/NoteTextViewer.tsx +++ b/packages/app-desktop/gui/NoteTextViewer.tsx @@ -5,7 +5,7 @@ import bridge from '../services/bridge'; import { focus } from '@joplin/lib/utils/focusHandler'; import { ForwardedRef, forwardRef, RefObject, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; import { WindowIdContext } from './NewWindowOrIFrame'; -import useDom from './hooks/useDom'; +import useDocument from './hooks/useDocument'; interface Props { // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied @@ -64,7 +64,7 @@ const NoteTextViewer = forwardRef((props: Props, ref: ForwardedRef void; const removePluginAssetsCallbackRef = useRef(null); - const parentDoc = useDom(webview); + const parentDoc = useDocument(webview); const containerWindow = parentDoc?.defaultView; useImperativeHandle(ref, () => { diff --git a/packages/app-desktop/gui/StyleSheets/StyleSheetContainer.tsx b/packages/app-desktop/gui/StyleSheets/StyleSheetContainer.tsx index 8f0ab0a8d03..f8ca4b363c6 100644 --- a/packages/app-desktop/gui/StyleSheets/StyleSheetContainer.tsx +++ b/packages/app-desktop/gui/StyleSheets/StyleSheetContainer.tsx @@ -14,7 +14,7 @@ import { useEffect, useMemo, useState } from 'react'; import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect'; import themeToCss from '@joplin/lib/services/style/themeToCss'; import { themeStyle } from '@joplin/lib/theme'; -import useDom from '../hooks/useDom'; +import useDocument from '../hooks/useDocument'; import { connect } from 'react-redux'; import { AppState } from '../../app.reducer'; @@ -97,7 +97,7 @@ const useAppliedCss = (doc: Document|null, css: string) => { const StyleSheetContainer: React.FC = props => { const [elementRef, setElementRef] = useState(null); - const doc = useDom(elementRef); + const doc = useDocument(elementRef); const themeCss = useThemeCss(props.themeId); const editorCss = useEditorCss(props.editorFontSetting); diff --git a/packages/app-desktop/gui/WindowCommandsAndDialogs/WindowCommandsAndDialogs.tsx b/packages/app-desktop/gui/WindowCommandsAndDialogs/WindowCommandsAndDialogs.tsx index e97ace2ce02..f8984dfceee 100644 --- a/packages/app-desktop/gui/WindowCommandsAndDialogs/WindowCommandsAndDialogs.tsx +++ b/packages/app-desktop/gui/WindowCommandsAndDialogs/WindowCommandsAndDialogs.tsx @@ -13,7 +13,7 @@ import { Dispatch } from 'redux'; import ModalMessageOverlay from './ModalMessageOverlay'; import { EditorNoteStatuses, stateUtils } from '@joplin/lib/reducer'; import dialogs from '../dialogs'; -import useDom from '../hooks/useDom'; +import useDocument from '../hooks/useDocument'; import useWindowCommands from './utils/useWindowCommands'; import PluginDialogs from './PluginDialogs'; import useSyncDialogState from './utils/useSyncDialogState'; @@ -73,7 +73,7 @@ const useSyncActiveWindow = (containerWindow: Window|null) => { const WindowCommandsAndDialogs: React.FC = props => { const [referenceElement, setReferenceElement] = useState(null); - const containerDocument = useDom(referenceElement); + const containerDocument = useDocument(referenceElement); const documentRef = useRef(null); documentRef.current = containerDocument; diff --git a/packages/app-desktop/gui/hooks/useDocument.ts b/packages/app-desktop/gui/hooks/useDocument.ts new file mode 100644 index 00000000000..f912d4142ef --- /dev/null +++ b/packages/app-desktop/gui/hooks/useDocument.ts @@ -0,0 +1,12 @@ +import { useMemo } from 'react'; + +// Returns the Document that contains [elementRef]. This is useful when a component +// can be rendered in secondary windows and needs to access the component's container +// document. +const useDocument = (elementRef: Element|null) => { + return useMemo(() => { + return (elementRef ?? null)?.getRootNode() as Document|null; + }, [elementRef]); +}; + +export default useDocument; diff --git a/packages/app-desktop/gui/hooks/useDom.ts b/packages/app-desktop/gui/hooks/useDom.ts deleted file mode 100644 index 2eb4bc7be4d..00000000000 --- a/packages/app-desktop/gui/hooks/useDom.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { useMemo } from 'react'; - -const useDom = (elementRef: Element|null) => { - return useMemo(() => { - return (elementRef ?? null)?.getRootNode() as Document|null; - }, [elementRef]); -}; - -export default useDom; From a2aa2fef5deb4555c9139dc23b4b61ff638244ae Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Tue, 5 Nov 2024 09:37:11 -0800 Subject: [PATCH 92/97] Fix Mermaid diagram download button The download button used bridge().window(...), which had been renamed to bridge().activeWindow(...). --- .../app-desktop/gui/NoteEditor/utils/useMessageHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.ts b/packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.ts index 8624f0e0132..5c15325bba3 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.ts @@ -5,7 +5,7 @@ import CommandService from '@joplin/lib/services/CommandService'; import PostMessageService from '@joplin/lib/services/PostMessageService'; import ResourceFetcher from '@joplin/lib/services/ResourceFetcher'; import { reg } from '@joplin/lib/registry'; -const bridge = require('@electron/remote').require('./bridge').default; +import bridge from '../../../services/bridge'; export default function useMessageHandler( scrollWhenReady: ScrollOptions|null, @@ -61,7 +61,7 @@ export default function useMessageHandler( mdToHtml, }, dispatch); - menu.popup({ window: bridge().window() }); + menu.popup({ window: bridge().activeWindow() }); } else if (msg.indexOf('#') === 0) { // This is an internal anchor, which is handled by the WebView so skip this case } else if (msg === 'contentScriptExecuteCommand') { From bbc42a666e26800fc5b0968807f71dc1d274f339 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Tue, 5 Nov 2024 11:11:54 -0800 Subject: [PATCH 93/97] Revert unnecessary change to app-mobile/package.json --- packages/app-mobile/package.json | 1 - yarn.lock | 10 ---------- 2 files changed, 11 deletions(-) diff --git a/packages/app-mobile/package.json b/packages/app-mobile/package.json index 955bdf8f4ed..e01a7211caf 100644 --- a/packages/app-mobile/package.json +++ b/packages/app-mobile/package.json @@ -100,7 +100,6 @@ "@tsconfig/react-native": "2.0.2", "@types/fs-extra": "11.0.4", "@types/jest": "29.5.12", - "@types/node": "18.19.43", "@types/react": "18.3.3", "@types/react-native": "0.70.6", "@types/react-redux": "7.1.33", diff --git a/yarn.lock b/yarn.lock index 1c34c01801f..860b9329201 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8223,7 +8223,6 @@ __metadata: "@tsconfig/react-native": 2.0.2 "@types/fs-extra": 11.0.4 "@types/jest": 29.5.12 - "@types/node": 18.19.43 "@types/react": 18.3.3 "@types/react-native": 0.70.6 "@types/react-redux": 7.1.33 @@ -13093,15 +13092,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:18.19.43": - version: 18.19.43 - resolution: "@types/node@npm:18.19.43" - dependencies: - undici-types: ~5.26.4 - checksum: 5eb9045aae6da86e8ad297381f93d29d2e7fcd4ed0c53670d9dff1e7b714920f8bbe5ee456289c19fc69c510ac197bdbacc7a785eaeba0afb9cb5d634a64bcd3 - languageName: node - linkType: hard - "@types/node@npm:^17.0.5": version: 17.0.45 resolution: "@types/node@npm:17.0.45" From d3b3beb1d0460ea2dd98a48c5878cbb339cd6de3 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Tue, 5 Nov 2024 11:20:27 -0800 Subject: [PATCH 94/97] Commenting --- packages/lib/BaseApplication.ts | 3 +++ packages/lib/eventManager.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/lib/BaseApplication.ts b/packages/lib/BaseApplication.ts index 527453caabe..2d9f07a80aa 100644 --- a/packages/lib/BaseApplication.ts +++ b/packages/lib/BaseApplication.ts @@ -511,6 +511,9 @@ export default class BaseApplication { refreshNotesUseSelectedNoteId = true; } + // Switching windows can also change which note(s) and which note parent type is selected. + // Refreshing notes after switching windows helps ensure that the selected note/tags/other state + // is correct for the current window. if (action.type === 'WINDOW_FOCUS' && action.lastWindowId !== action.windowId) { refreshNotes = true; refreshNotesUseSelectedNoteId = true; diff --git a/packages/lib/eventManager.ts b/packages/lib/eventManager.ts index 17bdcd0fad0..de762e88222 100644 --- a/packages/lib/eventManager.ts +++ b/packages/lib/eventManager.ts @@ -24,6 +24,9 @@ export enum EventName { interface ItemChangeEvent { itemType: ModelType; itemId: string; + // Passing a changeId to Note.save causes that changeId to be included + // in the corresponding ItemChangeEvent. This allows determining which + // call to Note.save triggered the event. changeId: string; eventType: number; } From 9919dcafc5cbdf79e9c89b4e5e846d86ddbd1837 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Tue, 5 Nov 2024 11:23:41 -0800 Subject: [PATCH 95/97] Remove throw new Error from openNoteInNewWindow command --- packages/app-desktop/commands/openNoteInNewWindow.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/app-desktop/commands/openNoteInNewWindow.ts b/packages/app-desktop/commands/openNoteInNewWindow.ts index 5ab595e9596..7683e635d76 100644 --- a/packages/app-desktop/commands/openNoteInNewWindow.ts +++ b/packages/app-desktop/commands/openNoteInNewWindow.ts @@ -17,9 +17,6 @@ export const runtime = (): CommandRuntime => { return { execute: async (context: CommandContext, noteId: string = null) => { noteId = noteId || stateUtils.selectedNoteId(context.state); - if (!noteId) { - throw new Error('Error: No notes are selected. Select at least one note to open in a new window.'); - } const note = await Note.load(noteId, { fields: ['parent_id'] }); context.dispatch({ From 8f8dfd9839d4338f2a3b93ac2abb4d3edd5b5866 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Tue, 5 Nov 2024 11:34:13 -0800 Subject: [PATCH 96/97] Don't duplicate cssUrls --- packages/app-desktop/gui/NewWindowOrIFrame.tsx | 6 ------ packages/app-desktop/index.html | 8 +------- packages/app-desktop/style.scss | 12 ++++++++++++ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/app-desktop/gui/NewWindowOrIFrame.tsx b/packages/app-desktop/gui/NewWindowOrIFrame.tsx index d8781233176..11d5d9e698b 100644 --- a/packages/app-desktop/gui/NewWindowOrIFrame.tsx +++ b/packages/app-desktop/gui/NewWindowOrIFrame.tsx @@ -99,12 +99,6 @@ const useDocumentSetup = (doc: Document|null, setLoaded: OnSetLoaded, onFocus?: const cssUrls = [ 'style.min.css', - 'style/icons/style.css', - 'vendor/lib/@fortawesome/fontawesome-free/css/all.min.css', - 'vendor/lib/react-datetime/css/react-datetime.css', - 'vendor/lib/smalltalk/css/smalltalk.css', - 'vendor/lib/roboto-fontface/css/roboto/roboto-fontface.css', - 'vendor/lib/codemirror/lib/codemirror.css', ]; for (const url of cssUrls) { diff --git a/packages/app-desktop/index.html b/packages/app-desktop/index.html index 797918d61bb..d5c39a52cec 100644 --- a/packages/app-desktop/index.html +++ b/packages/app-desktop/index.html @@ -8,14 +8,8 @@ --> Joplin + - - - - - - - diff --git a/packages/app-desktop/style.scss b/packages/app-desktop/style.scss index e6234dc2afd..259d6e0ff55 100644 --- a/packages/app-desktop/style.scss +++ b/packages/app-desktop/style.scss @@ -16,3 +16,15 @@ @use 'services/plugins/styles/index.scss' as plugins-styles; @use 'gui/styles/index.scss' as gui-styles; @use 'main.scss' as main; + +// These dynamic imports will be compiled to CSS imports in the output file. +// Prefer importing new CSS files here to linking them in index.html because +// this allows the CSS files to be included in secondary windows. +// +// See https://sasscss.org/documentation/at-rules/import#plain-css-imports. +@import url('style/icons/style.css'); +@import url('vendor/lib/@fortawesome/fontawesome-free/css/all.min.css'); +@import url('vendor/lib/react-datetime/css/react-datetime.css'); +@import url('vendor/lib/smalltalk/css/smalltalk.css'); +@import url('vendor/lib/roboto-fontface/css/roboto/roboto-fontface.css'); +@import url('vendor/lib/codemirror/lib/codemirror.css'); From f1a615767e4151f46f5fe3e5161a67d5cb8c7ac2 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Tue, 5 Nov 2024 11:44:41 -0800 Subject: [PATCH 97/97] Commenting --- packages/lib/reducer.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/lib/reducer.ts b/packages/lib/reducer.ts index 995391b5096..618681f2b32 100644 --- a/packages/lib/reducer.ts +++ b/packages/lib/reducer.ts @@ -121,6 +121,8 @@ export interface EditorNoteStatuses { } export interface State extends WindowState { + // Contains state specific to windows that currently don't have focus. + // See spec/background_windows.md for details. backgroundWindows: Record; folders: FolderEntity[];