diff --git a/.eslintignore b/.eslintignore index 945f3ce5fb8..0596c37a5a5 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 f948248b992..73598969782 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 1e4de49e0e0..3bad3cbce28 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.markdown-legacy'], 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 0becbad7b8e..6f1e50010d2 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 00000000000..4f46f58ae0d --- /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.ReactElement|string; + 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 00000000000..1d567275b35 --- /dev/null +++ b/packages/app-desktop/gui/NoteEditor/WarningBanner/WarningBanner.tsx @@ -0,0 +1,96 @@ +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.markdown-legacy', true); +}; + +const incompatiblePluginIds = [ + // cSpell:disable + 'com.septemberhx.Joplin.Enhancement', + 'ylc395.noteLinkSystem', + 'outline', + 'joplin.plugin.cmoptions', + 'plugin.calebjohn.MathMode', + // cSpell:enable +]; + +const onDismissLegacyEditorPrompt = () => { + Setting.setValue('editor.pluginCompatibilityBannerDismissedFor', [...PluginService.instance().pluginIds]); +}; + +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 showMarkdownPluginBanner = useMemo(() => { + if (props.bodyEditor === 'CodeMirror5') { + return false; + } + + const runningPluginIds = Object.keys(props.plugins); + + return runningPluginIds.some(id => { + if (props.pluginCompatibilityBannerDismissedFor.includes(id)) { + return false; + } + + return incompatiblePluginIds.includes(id); + }); + }, [props.bodyEditor, props.plugins, props.pluginCompatibilityBannerDismissedFor]); + + const markdownPluginBanner = ( + + {_('One or more installed plugins does not support the current markdown editor.')} + + ); + + 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 00000000000..6901cdbc67f --- /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 00000000000..ec1b07b1ea9 --- /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 00000000000..d4cc09a5eba --- /dev/null +++ b/packages/app-desktop/gui/NoteEditor/styles/warning-banner.scss @@ -0,0 +1,10 @@ + +.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; +} \ 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 8597a589122..85d0cb0695a 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/style.scss b/packages/app-desktop/style.scss index d561298ccd0..dfc9866e9e1 100644 --- a/packages/app-desktop/style.scss +++ b/packages/app-desktop/style.scss @@ -10,4 +10,5 @@ @use 'gui/NoteListHeader/style.scss' as note-list-header; @use 'gui/TrashNotification/style.scss' as trash-notification; @use 'gui/Sidebar/style.scss' as sidebar-styles; +@use 'gui/NoteEditor/style.scss'; @use 'main.scss' as main; \ No newline at end of file diff --git a/packages/lib/models/settings/builtInMetadata.ts b/packages/lib/models/settings/builtInMetadata.ts index 003b99add85..789f685658a 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.markdown-legacy': { + advanced: true, + value: false, + type: SettingItemType.Bool, + section: 'general', + public: true, + appTypes: [AppType.Desktop], + label: () => 'Use the legacy Markdown editor', + description: () => 'Switch to the legacy Markdown editor. Some plugins require this editor to function.', storage: SettingStorage.File, isGlobal: true, },