diff --git a/.eslintignore b/.eslintignore index 10672a2d3..1d7eb8ac8 100644 --- a/.eslintignore +++ b/.eslintignore @@ -296,6 +296,8 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js packages/app-desktop/gui/NoteEditor/NoteEditor.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 packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.js packages/app-desktop/gui/NoteEditor/commands/focusElementNoteTitle.js packages/app-desktop/gui/NoteEditor/commands/index.js diff --git a/.gitignore b/.gitignore index 710590954..dc20aaa8c 100644 --- a/.gitignore +++ b/.gitignore @@ -275,6 +275,8 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js packages/app-desktop/gui/NoteEditor/NoteEditor.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 packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.js packages/app-desktop/gui/NoteEditor/commands/focusElementNoteTitle.js packages/app-desktop/gui/NoteEditor/commands/index.js diff --git a/packages/app-desktop/gui/MainScreen/MainScreen.tsx b/packages/app-desktop/gui/MainScreen/MainScreen.tsx index 1e4de49e0..6901f37e2 100644 --- a/packages/app-desktop/gui/MainScreen/MainScreen.tsx +++ b/packages/app-desktop/gui/MainScreen/MainScreen.tsx @@ -85,7 +85,7 @@ interface Props { startupPluginsLoaded: boolean; shareInvitations: ShareInvitation[]; isSafeMode: boolean; - enableBetaMarkdownEditor: boolean; + enableLegacyMarkdownEditor: boolean; needApiAuth: boolean; processingShareInvitationResponse: boolean; isResettingLayout: boolean; @@ -783,12 +783,12 @@ class MainScreenComponent extends React.Component { }, editor: () => { - let bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror' : 'TinyMCE'; + let bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror6' : 'TinyMCE'; if (this.props.isSafeMode) { bodyEditor = 'PlainText'; - } else if (this.props.settingEditorCodeView && this.props.enableBetaMarkdownEditor) { - bodyEditor = 'CodeMirror6'; + } else if (this.props.settingEditorCodeView && this.props.enableLegacyMarkdownEditor) { + bodyEditor = 'CodeMirror5'; } return ; }, @@ -969,7 +969,7 @@ const mapStateToProps = (state: AppState) => { shareInvitations: state.shareService.shareInvitations, processingShareInvitationResponse: state.shareService.processingShareInvitationResponse, isSafeMode: state.settings.isSafeMode, - enableBetaMarkdownEditor: state.settings['editor.beta'], + enableLegacyMarkdownEditor: state.settings['editor.legacyMarkdown'], needApiAuth: state.needApiAuth, isResettingLayout: state.isResettingLayout, listRendererId: state.settings['notes.listRendererId'], diff --git a/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx b/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx index 8a9af4c46..e45146078 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteEditor.tsx @@ -35,7 +35,6 @@ 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 bridge from '../../services/bridge'; import NoteRevisionViewer from '../NoteRevisionViewer'; import { parseShareCache } from '@joplin/lib/services/share/reducer'; import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect'; @@ -51,6 +50,7 @@ import getPluginSettingValue from '@joplin/lib/services/plugins/utils/getPluginS 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 = [ @@ -434,7 +434,7 @@ function NoteEditor(props: NoteEditorProps) { editor = ; } else if (props.bodyEditor === 'PlainText') { editor = ; - } else if (props.bodyEditor === 'CodeMirror') { + } else if (props.bodyEditor === 'CodeMirror5') { editor = ; } else if (props.bodyEditor === 'CodeMirror6') { editor = ; @@ -442,22 +442,6 @@ function NoteEditor(props: NoteEditorProps) { throw new Error(`Invalid editor: ${props.bodyEditor}`); } - const onRichTextReadMoreLinkClick = useCallback(() => { - void bridge().openExternal('https://joplinapp.org/help/apps/rich_text_editor'); - }, []); - - const onRichTextDismissLinkClick = useCallback(() => { - Setting.setValue('richTextBannerDismissed', true); - }, []); - - const wysiwygBanner = props.bodyEditor !== 'TinyMCE' || props.richTextBannerDismissed ? null : ( -
- {_('This Rich Text editor has a number of limitations and it is recommended to be aware of them before using it.')} -   [ {_('Read more about it')} ] -   [ {_('Dismiss')} ] -
- ); - const noteRevisionViewer_onBack = useCallback(() => { setShowRevisions(false); }, []); @@ -612,7 +596,7 @@ function NoteEditor(props: NoteEditorProps) { {renderTagButton()} {renderTagBar()} - {wysiwygBanner} + ); @@ -636,7 +620,6 @@ const mapStateToProps = (state: AppState) => { syncStarted: state.syncStarted, decryptionStarted: state.decryptionWorker?.state !== 'idle', themeId: state.settings.theme, - richTextBannerDismissed: state.settings.richTextBannerDismissed, watchedNoteFiles: state.watchedNoteFiles, notesParentType: state.notesParentType, selectedNoteTags: state.selectedNoteTags, diff --git a/packages/app-desktop/gui/NoteEditor/WarningBanner/BannerContent.tsx b/packages/app-desktop/gui/NoteEditor/WarningBanner/BannerContent.tsx new file mode 100644 index 000000000..cc57f604e --- /dev/null +++ b/packages/app-desktop/gui/NoteEditor/WarningBanner/BannerContent.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { _ } from '@joplin/lib/locale'; + +interface Props { + children: React.ReactNode; + acceptMessage: string; + onAccept: ()=> void; + onDismiss: ()=> void; + visible: boolean; +} + +const BannerContent: React.FC = props => { + if (!props.visible) { + return null; + } + + return
+ {props.children} +   [ {props.acceptMessage} ] +   [ {_('Dismiss')} ] +
; +}; + +export default BannerContent; diff --git a/packages/app-desktop/gui/NoteEditor/WarningBanner/WarningBanner.tsx b/packages/app-desktop/gui/NoteEditor/WarningBanner/WarningBanner.tsx new file mode 100644 index 000000000..81c8f0f2c --- /dev/null +++ b/packages/app-desktop/gui/NoteEditor/WarningBanner/WarningBanner.tsx @@ -0,0 +1,104 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { AppState } from '../../../app.reducer'; +import Setting from '@joplin/lib/models/Setting'; +import BannerContent from './BannerContent'; +import { _ } from '@joplin/lib/locale'; +import bridge from '../../../services/bridge'; +import { useMemo } from 'react'; +import { PluginStates } from '@joplin/lib/services/plugins/reducer'; +import PluginService from '@joplin/lib/services/plugins/PluginService'; + +interface Props { + bodyEditor: string; + richTextBannerDismissed: boolean; + pluginCompatibilityBannerDismissedFor: string[]; + plugins: PluginStates; +} + +const onRichTextDismissLinkClick = () => { + Setting.setValue('richTextBannerDismissed', true); +}; + +const onRichTextReadMoreLinkClick = () => { + void bridge().openExternal('https://joplinapp.org/help/apps/rich_text_editor'); +}; + +const onSwitchToLegacyEditor = () => { + Setting.setValue('editor.legacyMarkdown', true); +}; + +const onDismissLegacyEditorPrompt = () => { + Setting.setValue('editor.pluginCompatibilityBannerDismissedFor', [...PluginService.instance().pluginIds]); +}; + +const incompatiblePluginIds = [ + // cSpell:disable + 'com.septemberhx.Joplin.Enhancement', + 'ylc395.noteLinkSystem', + 'outline', + 'joplin.plugin.cmoptions', + 'plugin.calebjohn.MathMode', + 'com.ckant.joplin-plugin-better-code-blocks', + // cSpell:enable +]; + +const WarningBanner: React.FC = props => { + const wysiwygBanner = ( + + {_('This Rich Text editor has a number of limitations and it is recommended to be aware of them before using it.')} + + ); + + const incompatiblePluginNames = useMemo(() => { + if (props.bodyEditor !== 'CodeMirror6') { + return []; + } + + const runningPluginIds = Object.keys(props.plugins); + + return runningPluginIds.map((id): string|string[] => { + if (props.pluginCompatibilityBannerDismissedFor?.includes(id)) { + return []; + } + + if (incompatiblePluginIds.includes(id)) { + return PluginService.instance().pluginById(id).manifest.name; + } else { + return []; + } + }).flat(); + }, [props.bodyEditor, props.plugins, props.pluginCompatibilityBannerDismissedFor]); + + const markdownPluginBanner = ( + 0} + > + {_('The following plugins may not support the current markdown editor:')} +
    + {incompatiblePluginNames.map((name, index) =>
  • {name}
  • )} +
+
+ ); + + return <> + {wysiwygBanner} + {markdownPluginBanner} + ; +}; + +export default connect((state: AppState) => { + return { + richTextBannerDismissed: state.settings.richTextBannerDismissed, + pluginCompatibilityBannerDismissedFor: state.settings['editor.pluginCompatibilityBannerDismissedFor'], + plugins: state.pluginService.plugins, + }; +})(WarningBanner); diff --git a/packages/app-desktop/gui/NoteEditor/style.scss b/packages/app-desktop/gui/NoteEditor/style.scss new file mode 100644 index 000000000..6901cdbc6 --- /dev/null +++ b/packages/app-desktop/gui/NoteEditor/style.scss @@ -0,0 +1,3 @@ + +@use "./styles/warning-banner.scss"; +@use "./styles/warning-banner-link.scss"; diff --git a/packages/app-desktop/gui/NoteEditor/styles/warning-banner-link.scss b/packages/app-desktop/gui/NoteEditor/styles/warning-banner-link.scss new file mode 100644 index 000000000..ec1b07b1e --- /dev/null +++ b/packages/app-desktop/gui/NoteEditor/styles/warning-banner-link.scss @@ -0,0 +1,6 @@ +.warning-banner-link { + color: var(--joplin-color); + font-family: var(--joplin-font-family); + font-size: var(--joplin-font-siize); + font-weight: bold; +} \ No newline at end of file diff --git a/packages/app-desktop/gui/NoteEditor/styles/warning-banner.scss b/packages/app-desktop/gui/NoteEditor/styles/warning-banner.scss new file mode 100644 index 000000000..39c33197f --- /dev/null +++ b/packages/app-desktop/gui/NoteEditor/styles/warning-banner.scss @@ -0,0 +1,13 @@ + +.warning-banner { + background: var(--joplin-warning-background-color); + font-family: var(--joplin-font-family); + padding: 10px; + font-size: var(--joplin-font-size); + line-height: 1.6em; + margin-top: 5px; + margin-bottom: 5px; + + max-height: 25vh; + overflow-y: auto; +} \ No newline at end of file diff --git a/packages/app-desktop/gui/NoteEditor/utils/types.ts b/packages/app-desktop/gui/NoteEditor/utils/types.ts index 8597a5891..85d0cb069 100644 --- a/packages/app-desktop/gui/NoteEditor/utils/types.ts +++ b/packages/app-desktop/gui/NoteEditor/utils/types.ts @@ -50,7 +50,6 @@ export interface NoteEditorProps { plugins: PluginStates; toolbarButtonInfos: ToolbarButtonInfo[]; setTagsToolbarButtonInfo: ToolbarButtonInfo; - richTextBannerDismissed: boolean; contentMaxWidth: number; isSafeMode: boolean; useCustomPdfViewer: boolean; diff --git a/packages/app-desktop/integration-tests/models/NoteEditorScreen.ts b/packages/app-desktop/integration-tests/models/NoteEditorScreen.ts index 28457abcc..fca779162 100644 --- a/packages/app-desktop/integration-tests/models/NoteEditorScreen.ts +++ b/packages/app-desktop/integration-tests/models/NoteEditorScreen.ts @@ -11,7 +11,7 @@ export default class NoteEditorPage { public constructor(private readonly page: Page) { this.containerLocator = page.locator('.rli-editor'); - this.codeMirrorEditor = this.containerLocator.locator('.codeMirrorEditor'); + this.codeMirrorEditor = this.containerLocator.locator('.cm-editor'); this.richTextEditor = this.containerLocator.locator('iframe[title="Rich Text Area"]'); this.noteTitleInput = this.containerLocator.locator('.title-input'); this.attachFileButton = this.containerLocator.locator('[title^="Attach file"]'); diff --git a/packages/app-desktop/style.scss b/packages/app-desktop/style.scss index c8e4008e5..55c2fcaf5 100644 --- a/packages/app-desktop/style.scss +++ b/packages/app-desktop/style.scss @@ -11,4 +11,5 @@ @use 'gui/TrashNotification/style.scss' as trash-notification; @use 'gui/Sidebar/style.scss' as sidebar-styles; @use 'gui/styles/index.scss'; -@use 'main.scss' as main; \ No newline at end of file +@use 'gui/NoteEditor/style.scss'; +@use 'main.scss' as main; diff --git a/packages/lib/models/settings/builtInMetadata.ts b/packages/lib/models/settings/builtInMetadata.ts index 003b99add..8406e0cb5 100644 --- a/packages/lib/models/settings/builtInMetadata.ts +++ b/packages/lib/models/settings/builtInMetadata.ts @@ -423,6 +423,13 @@ const builtInMetadata = (Setting: typeof SettingType) => { notesParent: { value: '', type: SettingItemType.String, public: false }, richTextBannerDismissed: { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: false }, + 'editor.pluginCompatibilityBannerDismissedFor': { + value: [] as string[], // List of plugin IDs + type: SettingItemType.Array, + storage: SettingStorage.File, + isGlobal: true, + public: false, + }, firstStart: { value: true, type: SettingItemType.Bool, public: false }, locale: { @@ -1195,8 +1202,8 @@ const builtInMetadata = (Setting: typeof SettingType) => { type: SettingItemType.Bool, public: true, appTypes: [AppType.Desktop], - label: () => 'Enable spell checking in Markdown editor? (WARNING BETA feature)', - description: () => 'Spell checker in the Markdown editor was previously unstable (cursor location was not stable, sometimes edits would not be saved or reflected in the viewer, etc.) however it appears to be more reliable now. If you notice any issue, please report it on GitHub or the Joplin Forum (Help -> Joplin Forum)', + label: () => _('Enable spell checking in Markdown editor?'), + description: () => _('Checks spelling in most non-code regions of the Markdown editor.'), storage: SettingStorage.File, isGlobal: true, }, @@ -1225,10 +1232,23 @@ const builtInMetadata = (Setting: typeof SettingType) => { value: false, type: SettingItemType.Bool, section: 'general', - public: true, + public: false, appTypes: [AppType.Desktop], label: () => 'Opt-in to the editor beta', - description: () => 'This beta adds improved accessibility and plugin API compatibility with the mobile editor. If you find bugs, please report them in the Discourse forum.', + description: () => 'Currently unused', + storage: SettingStorage.File, + isGlobal: true, + }, + + 'editor.legacyMarkdown': { + advanced: true, + value: false, + type: SettingItemType.Bool, + section: 'general', + public: true, + appTypes: [AppType.Desktop], + label: () => _('Use the legacy Markdown editor'), + description: () => _('Enable the the legacy Markdown editor. Some plugins require this editor to function. However, it has accessibility issues and other plugins will not work.'), storage: SettingStorage.File, isGlobal: true, },