From 59f8547a8de1c804cb2f01ac734c83268977fa28 Mon Sep 17 00:00:00 2001 From: Mo Date: Wed, 16 Nov 2022 05:54:32 -0600 Subject: [PATCH] feat(labs): super editor (#2001) --- .../blocks-editor/src/Editor/BlocksEditor.tsx | 7 +- .../src/Domain/Component/EditorIdentifier.ts | 5 + .../src/Domain/Component/NoteType.spec.ts | 17 ++ .../features/src/Domain/Component/NoteType.ts | 21 +- .../src/Domain/Feature/FeatureDescription.ts | 1 + .../src/Domain/Feature/FeatureIdentifier.ts | 7 +- .../src/Domain/Lists/ClientFeatures.ts | 9 +- .../src/Domain/Lists/ExperimentalFeatures.ts | 15 +- .../src/Domain/Permission/PermissionName.ts | 1 + packages/features/src/Domain/index.ts | 1 + .../Domain/Syncable/Component/Component.ts | 3 +- .../Syncable/Component/ComponentMutator.ts | 4 - .../src/Domain/Syncable/Tag/TagPreferences.ts | 4 +- .../src/Domain/Syncable/UserPrefs/PrefKey.ts | 4 +- .../Component/ComponentManagerInterface.ts | 2 +- .../lib/Client/NoteViewController.spec.ts | 34 --- packages/snjs/lib/Client/index.ts | 4 - .../ComponentManager/ComponentManager.ts | 6 +- .../lib/Services/Features/FeaturesService.ts | 23 +- .../Application/Application.spec.ts | 95 ++++++ .../javascripts/Application/Application.ts | 32 +- .../Application/VisibilityObserver.ts | 40 +++ .../BlockEditor/BlockEditorController.tsx | 22 -- .../ChangeEditor/ChangeEditorMenu.tsx | 4 +- .../Header/NewNotePreferences.tsx | 111 +++---- .../Header/NoteTitleFormatOptions.tsx | 20 ++ .../ListItemNotePreviewText.tsx | 3 - .../ContentListView/NoteListItem.tsx | 8 +- .../NoteGroupView/NoteGroupView.tsx | 4 +- .../Controller/EditorSaveTimeoutDebounce.ts | 5 + .../Controller}/FileViewController.ts | 2 +- .../Controller}/ItemGroupController.ts | 42 +-- .../ItemViewControllerInterface.ts | 0 .../Controller/NoteViewController.spec.ts | 83 +++++ .../Controller}/NoteViewController.ts | 162 ++++++---- .../TemplateNoteViewControllerOptions.ts | 2 +- .../Components/NoteView/NoteView.test.ts | 2 +- .../Components/NoteView/NoteView.tsx | 289 +++--------------- .../Components/NoteView/NoteViewProps.ts | 3 +- .../NoteView/PlainEditor/PlainEditor.tsx | 253 +++++++++++++++ .../Plugins/AutoLinkPlugin/AutoLinkPlugin.tsx | 0 .../BlockPickerPlugin/BlockPickerMenuItem.tsx | 0 .../BlockPickerPlugin/BlockPickerOption.tsx | 0 .../BlockPickerPlugin/BlockPickerPlugin.tsx | 0 .../BlockPickerPlugin/Blocks/Alignment.tsx | 0 .../BlockPickerPlugin/Blocks/BulletedList.tsx | 0 .../BlockPickerPlugin/Blocks/Checklist.tsx | 0 .../Plugins/BlockPickerPlugin/Blocks/Code.tsx | 0 .../BlockPickerPlugin/Blocks/Collapsible.tsx | 0 .../BlockPickerPlugin/Blocks/DateTime.tsx | 0 .../BlockPickerPlugin/Blocks/Divider.tsx | 0 .../BlockPickerPlugin/Blocks/Embeds.tsx | 0 .../BlockPickerPlugin/Blocks/Headings.tsx | 0 .../BlockPickerPlugin/Blocks/NumberedList.tsx | 0 .../BlockPickerPlugin/Blocks/Paragraph.tsx | 0 .../BlockPickerPlugin/Blocks/Quote.tsx | 0 .../BlockPickerPlugin/Blocks/Table.tsx | 0 .../ChangeContentCallback.tsx | 26 ++ .../SuperEditor}/Plugins/ClassNames.ts | 0 .../SuperEditor}/Plugins/Commands.ts | 0 .../Plugins/DateTimePlugin/DateTimePlugin.tsx | 0 .../Plugins/EncryptedFilePlugin/FilePlugin.ts | 2 +- .../Nodes/FileComponent.tsx | 0 .../EncryptedFilePlugin/Nodes/FileNode.tsx | 0 .../EncryptedFilePlugin/Nodes/FileUtils.tsx | 0 .../Nodes/SerializedFileNode.tsx | 0 .../Plugins/ImportPlugin/ImportPlugin.tsx | 0 .../ItemBubblePlugin/ItemBubblePlugin.ts | 0 .../Nodes/BubbleComponent.tsx | 0 .../ItemBubblePlugin/Nodes/BubbleNode.tsx | 0 .../ItemBubblePlugin/Nodes/BubbleUtils.tsx | 0 .../Nodes/SerializedBubbleNode.tsx | 0 .../SuperEditor}/Plugins/ItemNodeInterface.ts | 0 .../Plugins/ItemSelectionPlugin/ItemOption.ts | 0 .../ItemSelectionItemComponent.tsx | 0 .../ItemSelectionPlugin.tsx | 2 +- .../NodeObserverPlugin/NodeObserverPlugin.tsx | 0 .../SuperEditor/SuperEditor.tsx} | 69 ++++- .../SuperEditor}/SuperNoteImporter.tsx | 38 ++- .../CompoundPredicateBuilderController.ts | 4 +- .../src/javascripts/Constants/Constants.ts | 26 +- .../ItemList/ItemListController.ts | 20 +- .../Controllers/LinkingController.tsx | 2 +- packages/web/src/javascripts/FeatureTrunk.ts | 4 +- packages/web/src/javascripts/Logging.ts | 2 +- .../Utils/DropdownItemsForEditors.ts | 47 +-- .../Items/Icons/getIconAndTintForNoteType.ts | 5 +- .../Utils/createEditorMenuGroups.ts | 34 +-- .../__mocks__/@standardnotes/sncrypto-web.js | 5 + 89 files changed, 1021 insertions(+), 615 deletions(-) create mode 100644 packages/features/src/Domain/Component/EditorIdentifier.ts create mode 100644 packages/features/src/Domain/Component/NoteType.spec.ts delete mode 100644 packages/snjs/lib/Client/NoteViewController.spec.ts create mode 100644 packages/web/src/javascripts/Application/Application.spec.ts create mode 100644 packages/web/src/javascripts/Application/VisibilityObserver.ts delete mode 100644 packages/web/src/javascripts/Components/BlockEditor/BlockEditorController.tsx create mode 100644 packages/web/src/javascripts/Components/ContentListView/Header/NoteTitleFormatOptions.tsx create mode 100644 packages/web/src/javascripts/Components/NoteView/Controller/EditorSaveTimeoutDebounce.ts rename packages/{snjs/lib/Client => web/src/javascripts/Components/NoteView/Controller}/FileViewController.ts (95%) rename packages/{snjs/lib/Client => web/src/javascripts/Components/NoteView/Controller}/ItemGroupController.ts (70%) rename packages/{snjs/lib/Client => web/src/javascripts/Components/NoteView/Controller}/ItemViewControllerInterface.ts (100%) create mode 100644 packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.spec.ts rename packages/{snjs/lib/Client => web/src/javascripts/Components/NoteView/Controller}/NoteViewController.ts (61%) rename packages/{snjs/lib/Client => web/src/javascripts/Components/NoteView/Controller}/TemplateNoteViewControllerOptions.ts (81%) create mode 100644 packages/web/src/javascripts/Components/NoteView/PlainEditor/PlainEditor.tsx rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/AutoLinkPlugin/AutoLinkPlugin.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/BlockPickerPlugin/BlockPickerMenuItem.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/BlockPickerPlugin/BlockPickerOption.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/BlockPickerPlugin/BlockPickerPlugin.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/BlockPickerPlugin/Blocks/Alignment.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/BlockPickerPlugin/Blocks/BulletedList.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/BlockPickerPlugin/Blocks/Checklist.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/BlockPickerPlugin/Blocks/Code.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/BlockPickerPlugin/Blocks/Collapsible.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/BlockPickerPlugin/Blocks/DateTime.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/BlockPickerPlugin/Blocks/Divider.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/BlockPickerPlugin/Blocks/Embeds.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/BlockPickerPlugin/Blocks/Headings.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/BlockPickerPlugin/Blocks/NumberedList.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/BlockPickerPlugin/Blocks/Paragraph.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/BlockPickerPlugin/Blocks/Quote.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/BlockPickerPlugin/Blocks/Table.tsx (100%) create mode 100644 packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ChangeContentCallback/ChangeContentCallback.tsx rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/ClassNames.ts (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/Commands.ts (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/DateTimePlugin/DateTimePlugin.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/EncryptedFilePlugin/FilePlugin.ts (96%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/EncryptedFilePlugin/Nodes/FileComponent.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/EncryptedFilePlugin/Nodes/FileNode.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/EncryptedFilePlugin/Nodes/FileUtils.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/EncryptedFilePlugin/Nodes/SerializedFileNode.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/ImportPlugin/ImportPlugin.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/ItemBubblePlugin/ItemBubblePlugin.ts (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/ItemBubblePlugin/Nodes/BubbleComponent.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/ItemBubblePlugin/Nodes/BubbleNode.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/ItemBubblePlugin/Nodes/BubbleUtils.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/ItemBubblePlugin/Nodes/SerializedBubbleNode.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/ItemNodeInterface.ts (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/ItemSelectionPlugin/ItemOption.ts (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/ItemSelectionPlugin/ItemSelectionItemComponent.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/ItemSelectionPlugin/ItemSelectionPlugin.tsx (98%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/Plugins/NodeObserverPlugin/NodeObserverPlugin.tsx (100%) rename packages/web/src/javascripts/Components/{BlockEditor/BlockEditorComponent.tsx => NoteView/SuperEditor/SuperEditor.tsx} (55%) rename packages/web/src/javascripts/Components/{BlockEditor => NoteView/SuperEditor}/SuperNoteImporter.tsx (77%) create mode 100644 packages/web/src/javascripts/__mocks__/@standardnotes/sncrypto-web.js diff --git a/packages/blocks-editor/src/Editor/BlocksEditor.tsx b/packages/blocks-editor/src/Editor/BlocksEditor.tsx index e51ff3ca8..3360bbe12 100644 --- a/packages/blocks-editor/src/Editor/BlocksEditor.tsx +++ b/packages/blocks-editor/src/Editor/BlocksEditor.tsx @@ -39,6 +39,7 @@ type BlocksEditorProps = { children?: React.ReactNode; previewLength: number; spellcheck?: boolean; + ignoreFirstChange?: boolean; }; export const BlocksEditor: FunctionComponent = ({ @@ -47,11 +48,12 @@ export const BlocksEditor: FunctionComponent = ({ children, previewLength, spellcheck, + ignoreFirstChange = false, }) => { const [didIgnoreFirstChange, setDidIgnoreFirstChange] = useState(false); const handleChange = useCallback( (editorState: EditorState, _editor: LexicalEditor) => { - if (!didIgnoreFirstChange) { + if (ignoreFirstChange && !didIgnoreFirstChange) { setDidIgnoreFirstChange(true); return; } @@ -88,7 +90,7 @@ export const BlocksEditor: FunctionComponent = ({ {children} +
= ({ - diff --git a/packages/features/src/Domain/Component/EditorIdentifier.ts b/packages/features/src/Domain/Component/EditorIdentifier.ts new file mode 100644 index 000000000..82de7f959 --- /dev/null +++ b/packages/features/src/Domain/Component/EditorIdentifier.ts @@ -0,0 +1,5 @@ +import { FeatureIdentifier } from './../Feature/FeatureIdentifier' + +type ThirdPartyIdentifier = string + +export type EditorIdentifier = FeatureIdentifier | ThirdPartyIdentifier diff --git a/packages/features/src/Domain/Component/NoteType.spec.ts b/packages/features/src/Domain/Component/NoteType.spec.ts new file mode 100644 index 000000000..7d53b1420 --- /dev/null +++ b/packages/features/src/Domain/Component/NoteType.spec.ts @@ -0,0 +1,17 @@ +import { FeatureIdentifier } from '@standardnotes/features' +import { noteTypeForEditorIdentifier, NoteType } from './NoteType' + +describe('note type', () => { + it('should return the correct note type for editor identifier', () => { + expect(noteTypeForEditorIdentifier(FeatureIdentifier.PlainEditor)).toEqual(NoteType.Plain) + expect(noteTypeForEditorIdentifier(FeatureIdentifier.SuperEditor)).toEqual(NoteType.Super) + expect(noteTypeForEditorIdentifier(FeatureIdentifier.MarkdownVisualEditor)).toEqual(NoteType.Markdown) + expect(noteTypeForEditorIdentifier(FeatureIdentifier.MarkdownProEditor)).toEqual(NoteType.Markdown) + expect(noteTypeForEditorIdentifier(FeatureIdentifier.PlusEditor)).toEqual(NoteType.RichText) + expect(noteTypeForEditorIdentifier(FeatureIdentifier.CodeEditor)).toEqual(NoteType.Code) + expect(noteTypeForEditorIdentifier(FeatureIdentifier.SheetsEditor)).toEqual(NoteType.Spreadsheet) + expect(noteTypeForEditorIdentifier(FeatureIdentifier.TaskEditor)).toEqual(NoteType.Task) + expect(noteTypeForEditorIdentifier(FeatureIdentifier.TokenVaultEditor)).toEqual(NoteType.Authentication) + expect(noteTypeForEditorIdentifier('org.third.party')).toEqual(NoteType.Unknown) + }) +}) diff --git a/packages/features/src/Domain/Component/NoteType.ts b/packages/features/src/Domain/Component/NoteType.ts index af22ca9ef..269a66fdd 100644 --- a/packages/features/src/Domain/Component/NoteType.ts +++ b/packages/features/src/Domain/Component/NoteType.ts @@ -1,3 +1,7 @@ +import { FindNativeFeature } from '../Feature/Features' +import { FeatureIdentifier } from './../Feature/FeatureIdentifier' +import { EditorIdentifier } from './EditorIdentifier' + export enum NoteType { Authentication = 'authentication', Code = 'code', @@ -6,6 +10,21 @@ export enum NoteType { Spreadsheet = 'spreadsheet', Task = 'task', Plain = 'plain-text', - Blocks = 'blocks', + Super = 'super', Unknown = 'unknown', } + +export function noteTypeForEditorIdentifier(identifier: EditorIdentifier): NoteType { + if (identifier === FeatureIdentifier.PlainEditor) { + return NoteType.Plain + } else if (identifier === FeatureIdentifier.SuperEditor) { + return NoteType.Super + } + + const feature = FindNativeFeature(identifier as FeatureIdentifier) + if (feature && feature.note_type) { + return feature.note_type + } + + return NoteType.Unknown +} diff --git a/packages/features/src/Domain/Feature/FeatureDescription.ts b/packages/features/src/Domain/Feature/FeatureDescription.ts index b6fd23259..c4f648151 100644 --- a/packages/features/src/Domain/Feature/FeatureDescription.ts +++ b/packages/features/src/Domain/Feature/FeatureDescription.ts @@ -13,6 +13,7 @@ type RoleFields = { /** Statically populated. Non-influencing; used as a reference by other static consumers (such as email service) */ availableInSubscriptions: SubscriptionName[] + availableInRoles?: RoleName[] } export type BaseFeatureDescription = RoleFields & { diff --git a/packages/features/src/Domain/Feature/FeatureIdentifier.ts b/packages/features/src/Domain/Feature/FeatureIdentifier.ts index 0452e326a..26604b311 100644 --- a/packages/features/src/Domain/Feature/FeatureIdentifier.ts +++ b/packages/features/src/Domain/Feature/FeatureIdentifier.ts @@ -6,7 +6,6 @@ export enum FeatureIdentifier { DailyGDriveBackup = 'org.standardnotes.daily-gdrive-backup', DailyOneDriveBackup = 'org.standardnotes.daily-onedrive-backup', Files = 'org.standardnotes.files', - FilesBeta = 'org.standardnotes.files-beta', FilesLowStorageTier = 'org.standardnotes.files-low-storage-tier', FilesMaximumStorageTier = 'org.standardnotes.files-max-storage-tier', ListedCustomDomain = 'org.standardnotes.listed-custom-domain', @@ -28,10 +27,12 @@ export enum FeatureIdentifier { SolarizedDarkTheme = 'org.standardnotes.theme-solarized-dark', TitaniumTheme = 'org.standardnotes.theme-titanium', + PlainEditor = 'com.standardnotes.plain-text', + SuperEditor = 'com.standardnotes.super-editor', + CodeEditor = 'org.standardnotes.code-editor', MarkdownProEditor = 'org.standardnotes.advanced-markdown-editor', MarkdownVisualEditor = 'org.standardnotes.markdown-visual-editor', - PlainTextEditor = 'org.standardnotes.plain-text-editor', PlusEditor = 'org.standardnotes.plus-editor', SheetsEditor = 'org.standardnotes.standard-sheets', TaskEditor = 'org.standardnotes.simple-task-editor', @@ -50,4 +51,4 @@ export enum FeatureIdentifier { */ export const LegacyFileSafeIdentifier = 'org.standardnotes.legacy.file-safe' -export const ExperimentalFeatures = [] +export const ExperimentalFeatures = [FeatureIdentifier.SuperEditor] diff --git a/packages/features/src/Domain/Lists/ClientFeatures.ts b/packages/features/src/Domain/Lists/ClientFeatures.ts index c7e34f167..e081009c2 100644 --- a/packages/features/src/Domain/Lists/ClientFeatures.ts +++ b/packages/features/src/Domain/Lists/ClientFeatures.ts @@ -21,18 +21,11 @@ export function clientFeatures(): ClientFeatureDescription[] { }, { availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], - name: 'Encrypted files (coming soon)', + name: 'Encrypted files', identifier: FeatureIdentifier.Files, permission_name: PermissionName.Files, description: '', }, - { - availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], - name: 'Encrypted files beta', - identifier: FeatureIdentifier.FilesBeta, - permission_name: PermissionName.FilesBeta, - description: '', - }, { availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], name: 'Focus Mode', diff --git a/packages/features/src/Domain/Lists/ExperimentalFeatures.ts b/packages/features/src/Domain/Lists/ExperimentalFeatures.ts index 30c3ebf5a..9403655e6 100644 --- a/packages/features/src/Domain/Lists/ExperimentalFeatures.ts +++ b/packages/features/src/Domain/Lists/ExperimentalFeatures.ts @@ -1,5 +1,18 @@ +import { FeatureIdentifier } from './../Feature/FeatureIdentifier' +import { RoleName, SubscriptionName } from '@standardnotes/common' import { FeatureDescription } from '../Feature/FeatureDescription' +import { PermissionName } from '../Permission/PermissionName' export function experimentalFeatures(): FeatureDescription[] { - return [] + const superEditor: FeatureDescription = { + name: 'Super Notes', + identifier: FeatureIdentifier.SuperEditor, + availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan], + permission_name: PermissionName.SuperEditor, + description: + 'A new way to edit notes. Type / to bring up the block selection menu, or @ to embed images or link other tags and notes. Type - then space to start a list, or [] then space to start a checklist. Drag and drop an image or file to embed it in your note.', + availableInRoles: [RoleName.PlusUser, RoleName.ProUser], + } + + return [superEditor] } diff --git a/packages/features/src/Domain/Permission/PermissionName.ts b/packages/features/src/Domain/Permission/PermissionName.ts index 841f999d0..17fab4c39 100644 --- a/packages/features/src/Domain/Permission/PermissionName.ts +++ b/packages/features/src/Domain/Permission/PermissionName.ts @@ -39,4 +39,5 @@ export enum PermissionName { TokenVaultEditor = 'editor:token-vault', TwoFactorAuth = 'server:two-factor-auth', SubscriptionSharing = 'server:subscription-sharing', + SuperEditor = 'editor:super-editor', } diff --git a/packages/features/src/Domain/index.ts b/packages/features/src/Domain/index.ts index e132badb7..cc98d5888 100644 --- a/packages/features/src/Domain/index.ts +++ b/packages/features/src/Domain/index.ts @@ -11,3 +11,4 @@ export * from './Component/ComponentFlag' export * from './Component/ComponentPermission' export * from './Component/NoteType' export * from './Component/ThemeDockIcon' +export * from './Component/EditorIdentifier' diff --git a/packages/models/src/Domain/Syncable/Component/Component.ts b/packages/models/src/Domain/Syncable/Component/Component.ts index 29fed92bd..7383e2c55 100644 --- a/packages/models/src/Domain/Syncable/Component/Component.ts +++ b/packages/models/src/Domain/Syncable/Component/Component.ts @@ -114,7 +114,8 @@ export class SNComponent extends DecryptedItem implements Comp return this.content_type === ContentType.Theme || this.area === ComponentArea.Themes } - public isDefaultEditor(): boolean { + /** @deprecated Use global application PrefKey.DefaultEditorIdentifier */ + public legacyIsDefaultEditor(): boolean { return this.getAppDomainValue(AppDataField.DefaultEditor) === true } diff --git a/packages/models/src/Domain/Syncable/Component/ComponentMutator.ts b/packages/models/src/Domain/Syncable/Component/ComponentMutator.ts index 9b24c8aac..2a0e6f050 100644 --- a/packages/models/src/Domain/Syncable/Component/ComponentMutator.ts +++ b/packages/models/src/Domain/Syncable/Component/ComponentMutator.ts @@ -14,10 +14,6 @@ export class ComponentMutator extends DecryptedItemMutator { this.mutableContent.isMobileDefault = isMobileDefault } - set defaultEditor(defaultEditor: boolean) { - this.setAppDataItem(AppDataField.DefaultEditor, defaultEditor) - } - set componentData(componentData: Record) { this.mutableContent.componentData = componentData } diff --git a/packages/models/src/Domain/Syncable/Tag/TagPreferences.ts b/packages/models/src/Domain/Syncable/Tag/TagPreferences.ts index 5f85ceb2f..e61d01b7c 100644 --- a/packages/models/src/Domain/Syncable/Tag/TagPreferences.ts +++ b/packages/models/src/Domain/Syncable/Tag/TagPreferences.ts @@ -1,4 +1,4 @@ -import { FeatureIdentifier } from '@standardnotes/features' +import { EditorIdentifier } from '@standardnotes/features' import { NewNoteTitleFormat } from '../UserPrefs' import { CollectionSortProperty } from './../../Runtime/Collection/CollectionSort' @@ -15,7 +15,7 @@ export interface TagPreferences { hideEditorIcon?: boolean newNoteTitleFormat?: NewNoteTitleFormat customNoteTitleFormat?: string - editorIdentifier?: FeatureIdentifier | string + editorIdentifier?: EditorIdentifier entryMode?: 'normal' | 'daily' panelWidth?: number } diff --git a/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts b/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts index 27f1d4287..0346f876b 100644 --- a/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts +++ b/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts @@ -1,5 +1,5 @@ import { CollectionSortProperty } from '../../Runtime/Collection/CollectionSort' -import { FeatureIdentifier } from '@standardnotes/features' +import { EditorIdentifier, FeatureIdentifier } from '@standardnotes/features' export enum PrefKey { TagsPanelWidth = 'tagsPanelWidth', @@ -38,6 +38,7 @@ export enum PrefKey { CustomNoteTitleFormat = 'customNoteTitleFormat', UpdateSavingStatusIndicator = 'updateSavingStatusIndicator', DarkMode = 'darkMode', + DefaultEditorIdentifier = 'defaultEditorIdentifier', } export enum NewNoteTitleFormat { @@ -101,4 +102,5 @@ export type PrefValue = { [PrefKey.EditorFontSize]: EditorFontSize [PrefKey.UpdateSavingStatusIndicator]: boolean [PrefKey.DarkMode]: boolean + [PrefKey.DefaultEditorIdentifier]: EditorIdentifier } diff --git a/packages/services/src/Domain/Component/ComponentManagerInterface.ts b/packages/services/src/Domain/Component/ComponentManagerInterface.ts index 1ca330c45..e4332be4f 100644 --- a/packages/services/src/Domain/Component/ComponentManagerInterface.ts +++ b/packages/services/src/Domain/Component/ComponentManagerInterface.ts @@ -20,6 +20,6 @@ export interface ComponentManagerInterface { urlOverride?: string, ): ComponentViewerInterface presentPermissionsDialog(_dialog: PermissionDialog): void - getDefaultEditor(): SNComponent | undefined + legacyGetDefaultEditor(): SNComponent | undefined componentWithIdentifier(identifier: FeatureIdentifier | string): SNComponent | undefined } diff --git a/packages/snjs/lib/Client/NoteViewController.spec.ts b/packages/snjs/lib/Client/NoteViewController.spec.ts deleted file mode 100644 index dabe301d3..000000000 --- a/packages/snjs/lib/Client/NoteViewController.spec.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { SNApplication } from './../Application/Application' -import { ContentType } from '@standardnotes/common' -import { MutatorService } from './../Services/Mutator/MutatorService' -import { SNComponentManager } from './../Services/ComponentManager/ComponentManager' -import { NoteType } from '@standardnotes/features' -import { NoteViewController } from './NoteViewController' - -describe('note view controller', () => { - let application: SNApplication - - beforeEach(() => { - application = {} as jest.Mocked - application.streamItems = jest.fn() - - const componentManager = {} as jest.Mocked - componentManager.getDefaultEditor = jest.fn() - Object.defineProperty(application, 'componentManager', { value: componentManager }) - - const mutator = {} as jest.Mocked - mutator.createTemplateItem = jest.fn() - Object.defineProperty(application, 'mutator', { value: mutator }) - }) - - it('should create notes with plaintext note type', async () => { - const controller = new NoteViewController(application) - await controller.initialize(false) - - expect(application.mutator.createTemplateItem).toHaveBeenCalledWith( - ContentType.Note, - expect.objectContaining({ noteType: NoteType.Plain }), - expect.anything(), - ) - }) -}) diff --git a/packages/snjs/lib/Client/index.ts b/packages/snjs/lib/Client/index.ts index 2064237e9..1e306cf2c 100644 --- a/packages/snjs/lib/Client/index.ts +++ b/packages/snjs/lib/Client/index.ts @@ -1,5 +1 @@ -export * from './NoteViewController' -export * from './FileViewController' -export * from './ItemGroupController' export * from './ReactNativeToWebEvent' -export * from './TemplateNoteViewControllerOptions' diff --git a/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts b/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts index 50381b77a..1285497ae 100644 --- a/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts +++ b/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts @@ -619,7 +619,7 @@ export class SNComponentManager return editor } } - const defaultEditor = this.getDefaultEditor() + const defaultEditor = this.legacyGetDefaultEditor() if (defaultEditor && !defaultEditor.isExplicitlyDisabledForItem(note.uuid)) { return defaultEditor @@ -628,9 +628,9 @@ export class SNComponentManager } } - getDefaultEditor(): SNComponent | undefined { + legacyGetDefaultEditor(): SNComponent | undefined { const editors = this.componentsForArea(ComponentArea.Editor) - return editors.filter((e) => e.isDefaultEditor())[0] + return editors.filter((e) => e.legacyIsDefaultEditor())[0] } permissionsStringForPermissions(permissions: ComponentPermission[], component: SNComponent): string { diff --git a/packages/snjs/lib/Services/Features/FeaturesService.ts b/packages/snjs/lib/Services/Features/FeaturesService.ts index b85f1fb04..f03cf1d3d 100644 --- a/packages/snjs/lib/Services/Features/FeaturesService.ts +++ b/packages/snjs/lib/Services/Features/FeaturesService.ts @@ -173,24 +173,19 @@ export class SNFeaturesService public enableExperimentalFeature(identifier: FeaturesImports.FeatureIdentifier): void { const feature = this.getUserFeature(identifier) - if (!feature) { - throw Error('Attempting to enable a feature user does not have access to.') - } this.enabledExperimentalFeatures.push(identifier) void this.storageService.setValue(StorageKey.ExperimentalFeatures, this.enabledExperimentalFeatures) - void this.mapRemoteNativeFeaturesToItems([feature]) + if (feature) { + void this.mapRemoteNativeFeaturesToItems([feature]) + } + void this.notifyEvent(FeaturesEvent.FeaturesUpdated) } public disableExperimentalFeature(identifier: FeaturesImports.FeatureIdentifier): void { - const feature = this.getUserFeature(identifier) - if (!feature) { - throw Error('Attempting to disable a feature user does not have access to.') - } - removeFromArray(this.enabledExperimentalFeatures, identifier) void this.storageService.setValue(StorageKey.ExperimentalFeatures, this.enabledExperimentalFeatures) @@ -486,6 +481,16 @@ export class SNFeaturesService return FeatureStatus.Entitled } + if (this.isExperimentalFeature(featureId)) { + const nativeFeature = FeaturesImports.FindNativeFeature(featureId) + if (nativeFeature) { + const hasRole = this.roles.some((role) => nativeFeature.availableInRoles?.includes(role)) + if (hasRole) { + return FeatureStatus.Entitled + } + } + } + const isDeprecated = this.isFeatureDeprecated(featureId) if (isDeprecated) { if (this.hasPaidOnlineOrOfflineSubscription()) { diff --git a/packages/web/src/javascripts/Application/Application.spec.ts b/packages/web/src/javascripts/Application/Application.spec.ts new file mode 100644 index 000000000..e8b29fca5 --- /dev/null +++ b/packages/web/src/javascripts/Application/Application.spec.ts @@ -0,0 +1,95 @@ +/** + * @jest-environment jsdom + */ + +import { + Environment, + FeatureIdentifier, + namespacedKey, + Platform, + RawStorageKey, + SNComponent, + SNComponentManager, + SNLog, + SNTag, +} from '@standardnotes/snjs' +import { WebApplication } from '@/Application/Application' +import { WebOrDesktopDevice } from './Device/WebOrDesktopDevice' + +describe('web application', () => { + let application: WebApplication + let componentManager: SNComponentManager + + // eslint-disable-next-line no-console + SNLog.onLog = console.log + SNLog.onError = console.error + + beforeEach(() => { + const identifier = '123' + + window.matchMedia = jest.fn().mockReturnValue({ matches: true, addListener: jest.fn() }) + + const device = { + environment: Environment.Desktop, + appVersion: '1.2.3', + setApplication: jest.fn(), + openDatabase: jest.fn().mockReturnValue(Promise.resolve()), + getRawStorageValue: jest.fn().mockImplementation((key) => { + if (key === namespacedKey(identifier, RawStorageKey.SnjsVersion)) { + return '10.0.0' + } + return undefined + }), + setRawStorageValue: jest.fn(), + } as unknown as jest.Mocked + + application = new WebApplication(device, Platform.MacWeb, identifier, 'https://sync', 'https://socket') + + componentManager = {} as jest.Mocked + componentManager.legacyGetDefaultEditor = jest.fn() + Object.defineProperty(application, 'componentManager', { value: componentManager }) + + application.prepareForLaunch({ receiveChallenge: jest.fn() }) + }) + + describe('geDefaultEditorIdentifier', () => { + it('should return plain editor if no default tag editor or component editor', () => { + const editorIdentifier = application.geDefaultEditorIdentifier() + + expect(editorIdentifier).toEqual(FeatureIdentifier.PlainEditor) + }) + + it('should return pref key based value if available', () => { + application.getPreference = jest.fn().mockReturnValue(FeatureIdentifier.SuperEditor) + + const editorIdentifier = application.geDefaultEditorIdentifier() + + expect(editorIdentifier).toEqual(FeatureIdentifier.SuperEditor) + }) + + it('should return default tag identifier if tag supplied', () => { + const tag = { + preferences: { + editorIdentifier: FeatureIdentifier.SuperEditor, + }, + } as jest.Mocked + + const editorIdentifier = application.geDefaultEditorIdentifier(tag) + + expect(editorIdentifier).toEqual(FeatureIdentifier.SuperEditor) + }) + + it('should return legacy editor identifier', () => { + const editor = { + legacyIsDefaultEditor: jest.fn().mockReturnValue(true), + identifier: FeatureIdentifier.MarkdownProEditor, + } as unknown as jest.Mocked + + componentManager.legacyGetDefaultEditor = jest.fn().mockReturnValue(editor) + + const editorIdentifier = application.geDefaultEditorIdentifier() + + expect(editorIdentifier).toEqual(FeatureIdentifier.MarkdownProEditor) + }) + }) +}) diff --git a/packages/web/src/javascripts/Application/Application.ts b/packages/web/src/javascripts/Application/Application.ts index 6335c5af5..7413e5545 100644 --- a/packages/web/src/javascripts/Application/Application.ts +++ b/packages/web/src/javascripts/Application/Application.ts @@ -5,7 +5,6 @@ import { DeinitSource, Platform, SNApplication, - ItemGroupController, removeFromArray, DesktopDeviceInterface, isDesktopDevice, @@ -20,6 +19,8 @@ import { MobileUnlockTiming, InternalEventBus, DecryptedItem, + EditorIdentifier, + FeatureIdentifier, } from '@standardnotes/snjs' import { makeObservable, observable } from 'mobx' import { PanelResizedData } from '@/Types/PanelResizedData' @@ -40,6 +41,8 @@ import { PrefDefaults } from '@/Constants/PrefDefaults' import { setCustomViewportHeight } from '@/setViewportHeightWithFallback' import { WebServices } from './WebServices' import { FeatureName } from '@/Controllers/FeatureName' +import { ItemGroupController } from '@/Components/NoteView/Controller/ItemGroupController' +import { VisibilityObserver } from './VisibilityObserver' export type WebEventObserver = (event: WebAppEvent, data?: unknown) => void @@ -47,10 +50,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter private webServices!: WebServices private webEventObservers: WebEventObserver[] = [] public itemControllerGroup: ItemGroupController - private onVisibilityChange: () => void private mobileWebReceiver?: MobileWebReceiver private androidBackHandler?: AndroidBackHandler public readonly routeService: RouteServiceInterface + private visibilityObserver?: VisibilityObserver constructor( deviceInterface: WebOrDesktopDevice, @@ -106,14 +109,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter } } - this.onVisibilityChange = () => { - const visible = document.visibilityState === 'visible' - const event = visible ? WebAppEvent.WindowDidFocus : WebAppEvent.WindowDidBlur - this.notifyWebEvent(event) - } - if (!isDesktopApplication()) { - document.addEventListener('visibilitychange', this.onVisibilityChange) + this.visibilityObserver = new VisibilityObserver((event) => { + this.notifyWebEvent(event) + }) } } @@ -144,8 +143,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter this.webEventObservers.length = 0 - document.removeEventListener('visibilitychange', this.onVisibilityChange) - ;(this.onVisibilityChange as unknown) = undefined + if (this.visibilityObserver) { + this.visibilityObserver.deinit() + this.visibilityObserver = undefined + } } catch (error) { console.error('Error while deiniting application', error) } @@ -379,4 +380,13 @@ export class WebApplication extends SNApplication implements WebApplicationInter showAccountMenu(): void { this.getViewControllerManager().accountMenuController.setShow(true) } + + geDefaultEditorIdentifier(currentTag?: SNTag): EditorIdentifier { + return ( + currentTag?.preferences?.editorIdentifier || + this.getPreference(PrefKey.DefaultEditorIdentifier) || + this.componentManager.legacyGetDefaultEditor()?.identifier || + FeatureIdentifier.PlainEditor + ) + } } diff --git a/packages/web/src/javascripts/Application/VisibilityObserver.ts b/packages/web/src/javascripts/Application/VisibilityObserver.ts new file mode 100644 index 000000000..f610b39f2 --- /dev/null +++ b/packages/web/src/javascripts/Application/VisibilityObserver.ts @@ -0,0 +1,40 @@ +import { WebAppEvent } from '@standardnotes/snjs' + +export class VisibilityObserver { + private raceTimeout?: ReturnType + + constructor(private onEvent: (event: WebAppEvent) => void) { + /** + * Browsers may handle focus and visibilitychange events differently. + * Focus better handles window focus events but may not handle tab switching. + * We will listen for both and debouce notifying so that the most recent event wins. + */ + document.addEventListener('visibilitychange', this.onVisibilityChange) + window.addEventListener('focus', this.onFocusEvent, false) + } + + onVisibilityChange = () => { + const visible = document.visibilityState === 'visible' + const event = visible ? WebAppEvent.WindowDidFocus : WebAppEvent.WindowDidBlur + this.notifyEvent(event) + } + + onFocusEvent = () => { + this.notifyEvent(WebAppEvent.WindowDidFocus) + } + + private notifyEvent(event: WebAppEvent): void { + if (this.raceTimeout) { + clearTimeout(this.raceTimeout) + } + this.raceTimeout = setTimeout(() => { + this.onEvent(event) + }, 250) + } + + deinit(): void { + document.removeEventListener('visibilitychange', this.onVisibilityChange) + window.removeEventListener('focus', this.onFocusEvent) + ;(this.onEvent as unknown) = undefined + } +} diff --git a/packages/web/src/javascripts/Components/BlockEditor/BlockEditorController.tsx b/packages/web/src/javascripts/Components/BlockEditor/BlockEditorController.tsx deleted file mode 100644 index b8d4b261e..000000000 --- a/packages/web/src/javascripts/Components/BlockEditor/BlockEditorController.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { WebApplication } from '@/Application/Application' -import { NoteMutator, SNNote } from '@standardnotes/snjs' - -export class BlockEditorController { - constructor(private note: SNNote, private application: WebApplication) { - this.note = note - this.application = application - } - - deinit() { - ;(this.note as unknown) = undefined - ;(this.application as unknown) = undefined - } - - async save(values: { text: string; previewPlain: string; previewHtml?: string }): Promise { - await this.application.mutator.changeAndSaveItem(this.note, (mutator) => { - mutator.text = values.text - mutator.preview_plain = values.previewPlain - mutator.preview_html = values.previewHtml - }) - } -} diff --git a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx index a705543c7..f9c042934 100644 --- a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx +++ b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx @@ -12,7 +12,7 @@ import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem' import { createEditorMenuGroups } from '../../Utils/createEditorMenuGroups' import { reloadFont } from '../NoteView/FontFunctions' import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon' -import { SuperNoteImporter } from '../BlockEditor/SuperNoteImporter' +import { SuperNoteImporter } from '../NoteView/SuperEditor/SuperNoteImporter' type ChangeEditorMenuProps = { application: WebApplication @@ -114,7 +114,7 @@ const ChangeEditorMenu: FunctionComponent = ({ return } - if (itemToBeSelected.noteType === NoteType.Blocks) { + if (itemToBeSelected.noteType === NoteType.Super) { setPendingSuperItem(itemToBeSelected) handleDisableClickoutsideRequest?.() setShowSuperImporter(true) diff --git a/packages/web/src/javascripts/Components/ContentListView/Header/NewNotePreferences.tsx b/packages/web/src/javascripts/Components/ContentListView/Header/NewNotePreferences.tsx index 0b28c855c..1618fde48 100644 --- a/packages/web/src/javascripts/Components/ContentListView/Header/NewNotePreferences.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/Header/NewNotePreferences.tsx @@ -1,12 +1,4 @@ -import { - ComponentArea, - ComponentMutator, - FeatureIdentifier, - NewNoteTitleFormat, - PrefKey, - SNComponent, - TagPreferences, -} from '@standardnotes/snjs' +import { FeatureIdentifier, NewNoteTitleFormat, PrefKey, EditorIdentifier, TagPreferences } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { ChangeEventHandler, FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' import { PrefDefaults } from '@/Constants/PrefDefaults' @@ -16,31 +8,14 @@ import { WebApplication } from '@/Application/Application' import { AnyTag } from '@/Controllers/Navigation/AnyTagType' import { PreferenceMode } from './PreferenceMode' import dayjs from 'dayjs' -import { getDropdownItemsForAllEditors, PlainEditorType } from '@/Utils/DropdownItemsForEditors' +import { EditorOption, getDropdownItemsForAllEditors } from '@/Utils/DropdownItemsForEditors' +import { classNames } from '@/Utils/ConcatenateClassNames' +import { NoteTitleFormatOptions } from './NoteTitleFormatOptions' const PrefChangeDebounceTimeInMs = 25 const HelpPageUrl = 'https://day.js.org/docs/en/display/format#list-of-all-available-formats' -const NoteTitleFormatOptions = [ - { - label: 'Current date and time', - value: NewNoteTitleFormat.CurrentDateAndTime, - }, - { - label: 'Current note count', - value: NewNoteTitleFormat.CurrentNoteCount, - }, - { - label: 'Custom format', - value: NewNoteTitleFormat.CustomFormat, - }, - { - label: 'Empty', - value: NewNoteTitleFormat.Empty, - }, -] - type Props = { application: WebApplication selectedTag: AnyTag @@ -57,22 +32,24 @@ const NewNotePreferences: FunctionComponent = ({ disabled, }: Props) => { const [editorItems, setEditorItems] = useState([]) - const [defaultEditorIdentifier, setDefaultEditorIdentifier] = useState(PlainEditorType) + const [defaultEditorIdentifier, setDefaultEditorIdentifier] = useState( + FeatureIdentifier.PlainEditor, + ) const [newNoteTitleFormat, setNewNoteTitleFormat] = useState( NewNoteTitleFormat.CurrentDateAndTime, ) const [customNoteTitleFormat, setCustomNoteTitleFormat] = useState('') - const getGlobalEditorDefault = useCallback((): SNComponent | undefined => { - return application.componentManager.componentsForArea(ComponentArea.Editor).filter((e) => e.isDefaultEditor())[0] + const getGlobalEditorDefaultIdentifier = useCallback((): string => { + return application.geDefaultEditorIdentifier() }, [application]) const reloadPreferences = useCallback(() => { if (mode === 'tag' && selectedTag.preferences?.editorIdentifier) { setDefaultEditorIdentifier(selectedTag.preferences?.editorIdentifier) } else { - const globalDefault = getGlobalEditorDefault() - setDefaultEditorIdentifier(globalDefault?.identifier || PlainEditorType) + const globalDefault = getGlobalEditorDefaultIdentifier() + setDefaultEditorIdentifier(globalDefault) } if (mode === 'tag' && selectedTag.preferences?.newNoteTitleFormat) { @@ -82,7 +59,14 @@ const NewNotePreferences: FunctionComponent = ({ application.getPreference(PrefKey.NewNoteTitleFormat, PrefDefaults[PrefKey.NewNoteTitleFormat]), ) } - }, [mode, selectedTag, application, getGlobalEditorDefault, setDefaultEditorIdentifier, setNewNoteTitleFormat]) + }, [ + mode, + selectedTag, + application, + getGlobalEditorDefaultIdentifier, + setDefaultEditorIdentifier, + setNewNoteTitleFormat, + ]) useEffect(() => { if (mode === 'tag' && selectedTag.preferences?.customNoteTitleFormat) { @@ -107,52 +91,22 @@ const NewNotePreferences: FunctionComponent = ({ } } - const removeEditorGlobalDefault = (application: WebApplication, component: SNComponent) => { - application.mutator - .changeAndSaveItem(component, (m) => { - const mutator = m as ComponentMutator - mutator.defaultEditor = false - }) - .catch(console.error) - } - - const makeEditorGlobalDefault = ( - application: WebApplication, - component: SNComponent, - currentDefault?: SNComponent, - ) => { - if (currentDefault) { - removeEditorGlobalDefault(application, currentDefault) - } - application.mutator - .changeAndSaveItem(component, (m) => { - const mutator = m as ComponentMutator - mutator.defaultEditor = true - }) - .catch(console.error) - } - useEffect(() => { setEditorItems(getDropdownItemsForAllEditors(application)) }, [application]) - const setDefaultEditor = (value: string) => { - setDefaultEditorIdentifier(value as FeatureIdentifier) + const setDefaultEditor = useCallback( + (value: EditorOption['value']) => { + setDefaultEditorIdentifier(value as FeatureIdentifier) - if (mode === 'global') { - const editors = application.componentManager.componentsForArea(ComponentArea.Editor) - const currentDefault = getGlobalEditorDefault() - - if (value !== PlainEditorType) { - const editorComponent = editors.filter((e) => e.package_info.identifier === value)[0] - makeEditorGlobalDefault(application, editorComponent, currentDefault) - } else if (currentDefault) { - removeEditorGlobalDefault(application, currentDefault) + if (mode === 'global') { + void application.setPreference(PrefKey.DefaultEditorIdentifier, value) + } else { + void changePreferencesCallback({ editorIdentifier: value }) } - } else { - void changePreferencesCallback({ editorIdentifier: value }) - } - } + }, + [application, changePreferencesCallback, mode], + ) const debounceTimeoutRef = useRef() @@ -187,7 +141,7 @@ const NewNotePreferences: FunctionComponent = ({ label="Select the default note type" items={editorItems} value={defaultEditorIdentifier} - onChange={setDefaultEditor} + onChange={(value) => setDefaultEditor(value as EditorOption['value'])} />
@@ -211,7 +165,10 @@ const NewNotePreferences: FunctionComponent = ({
= ({ item, hidePreview, {!item.preview_html && item.preview_plain && (
{item.preview_plain}
)} - {!item.preview_html && !item.preview_plain && item.text && ( -
{item.text}
- )}
) } diff --git a/packages/web/src/javascripts/Components/ContentListView/NoteListItem.tsx b/packages/web/src/javascripts/Components/ContentListView/NoteListItem.tsx index 7d710a4bf..41d30249e 100644 --- a/packages/web/src/javascripts/Components/ContentListView/NoteListItem.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/NoteListItem.tsx @@ -1,4 +1,3 @@ -import { PLAIN_EDITOR_NAME } from '@/Constants/Constants' import { isFile, SNNote } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { FunctionComponent, useCallback, useRef } from 'react' @@ -36,9 +35,8 @@ const NoteListItem: FunctionComponent> = ({ const listItemRef = useRef(null) - const editorForNote = application.componentManager.editorForNote(item as SNNote) - const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME - const [icon, tint] = getIconAndTintForNoteType(editorForNote?.package_info.note_type) + const noteType = item.noteType || application.componentManager.editorForNote(item)?.package_info.note_type + const [icon, tint] = getIconAndTintForNoteType(noteType) const hasFiles = application.items.itemsReferencingItem(item).filter(isFile).length > 0 const openNoteContextMenu = (posX: number, posY: number) => { @@ -93,7 +91,7 @@ const NoteListItem: FunctionComponent> = ({ > {!hideIcon ? (
- +
) : (
diff --git a/packages/web/src/javascripts/Components/NoteGroupView/NoteGroupView.tsx b/packages/web/src/javascripts/Components/NoteGroupView/NoteGroupView.tsx index 7b17b2387..6f8495f81 100644 --- a/packages/web/src/javascripts/Components/NoteGroupView/NoteGroupView.tsx +++ b/packages/web/src/javascripts/Components/NoteGroupView/NoteGroupView.tsx @@ -1,4 +1,4 @@ -import { FileItem, FileViewController, NoteViewController } from '@standardnotes/snjs' +import { FileItem } from '@standardnotes/snjs' import { AbstractComponent } from '@/Components/Abstract/PureComponent' import { WebApplication } from '@/Application/Application' import MultipleSelectedNotes from '@/Components/MultipleSelectedNotes/MultipleSelectedNotes' @@ -8,6 +8,8 @@ import { AppPaneId } from '../ResponsivePane/AppPaneMetadata' import ResponsivePaneContent from '../ResponsivePane/ResponsivePaneContent' import FileView from '../FileView/FileView' import NoteView from '../NoteView/NoteView' +import { NoteViewController } from '../NoteView/Controller/NoteViewController' +import { FileViewController } from '../NoteView/Controller/FileViewController' type State = { showMultipleSelectedNotes: boolean diff --git a/packages/web/src/javascripts/Components/NoteView/Controller/EditorSaveTimeoutDebounce.ts b/packages/web/src/javascripts/Components/NoteView/Controller/EditorSaveTimeoutDebounce.ts new file mode 100644 index 000000000..836d6fc6f --- /dev/null +++ b/packages/web/src/javascripts/Components/NoteView/Controller/EditorSaveTimeoutDebounce.ts @@ -0,0 +1,5 @@ +export const EditorSaveTimeoutDebounce = { + Desktop: 350, + ImmediateChange: 100, + NativeMobileWeb: 700, +} diff --git a/packages/snjs/lib/Client/FileViewController.ts b/packages/web/src/javascripts/Components/NoteView/Controller/FileViewController.ts similarity index 95% rename from packages/snjs/lib/Client/FileViewController.ts rename to packages/web/src/javascripts/Components/NoteView/Controller/FileViewController.ts index c126b390c..85415f7b3 100644 --- a/packages/snjs/lib/Client/FileViewController.ts +++ b/packages/web/src/javascripts/Components/NoteView/Controller/FileViewController.ts @@ -1,6 +1,6 @@ import { FileItem } from '@standardnotes/models' import { ContentType } from '@standardnotes/common' -import { SNApplication } from '../Application/Application' +import { SNApplication } from '@standardnotes/snjs' import { ItemViewControllerInterface } from './ItemViewControllerInterface' export class FileViewController implements ItemViewControllerInterface { diff --git a/packages/snjs/lib/Client/ItemGroupController.ts b/packages/web/src/javascripts/Components/NoteView/Controller/ItemGroupController.ts similarity index 70% rename from packages/snjs/lib/Client/ItemGroupController.ts rename to packages/web/src/javascripts/Components/NoteView/Controller/ItemGroupController.ts index bf382d6ba..1cbf6aac0 100644 --- a/packages/snjs/lib/Client/ItemGroupController.ts +++ b/packages/web/src/javascripts/Components/NoteView/Controller/ItemGroupController.ts @@ -1,32 +1,18 @@ -import { FileItem, PrefKey, SNNote } from '@standardnotes/models' +import { WebApplication } from '@/Application/Application' import { removeFromArray } from '@standardnotes/utils' -import { ApplicationEvent } from '@standardnotes/services' - -import { SNApplication } from '../Application/Application' - +import { FileItem, SNNote } from '@standardnotes/snjs' import { NoteViewController } from './NoteViewController' import { FileViewController } from './FileViewController' import { TemplateNoteViewControllerOptions } from './TemplateNoteViewControllerOptions' type ItemControllerGroupChangeCallback = (activeController: NoteViewController | FileViewController | undefined) => void -type CreateItemControllerOptions = FileItem | SNNote | TemplateNoteViewControllerOptions - export class ItemGroupController { public itemControllers: (NoteViewController | FileViewController)[] = [] - private addTagHierarchy: boolean changeObservers: ItemControllerGroupChangeCallback[] = [] eventObservers: (() => void)[] = [] - constructor(private application: SNApplication) { - this.addTagHierarchy = application.getPreference(PrefKey.NoteAddToParentFolders, true) - - this.eventObservers.push( - application.addSingleEventObserver(ApplicationEvent.PreferencesChanged, async () => { - this.addTagHierarchy = application.getPreference(PrefKey.NoteAddToParentFolders, true) - }), - ) - } + constructor(private application: WebApplication) {} public deinit(): void { ;(this.application as unknown) = undefined @@ -44,26 +30,30 @@ export class ItemGroupController { this.itemControllers.length = 0 } - async createItemController(options: CreateItemControllerOptions): Promise { + async createItemController(context: { + file?: FileItem + note?: SNNote + templateOptions?: TemplateNoteViewControllerOptions + }): Promise { if (this.activeItemViewController) { this.closeItemController(this.activeItemViewController, { notify: false }) } let controller!: NoteViewController | FileViewController - if (options instanceof FileItem) { - const file = options - controller = new FileViewController(this.application, file) - } else if (options instanceof SNNote) { - const note = options - controller = new NoteViewController(this.application, note) + if (context.file) { + controller = new FileViewController(this.application, context.file) + } else if (context.note) { + controller = new NoteViewController(this.application, context.note) + } else if (context.templateOptions) { + controller = new NoteViewController(this.application, undefined, context.templateOptions) } else { - controller = new NoteViewController(this.application, undefined, options) + throw Error('Invalid input to createItemController') } this.itemControllers.push(controller) - await controller.initialize(this.addTagHierarchy) + await controller.initialize() this.notifyObservers() diff --git a/packages/snjs/lib/Client/ItemViewControllerInterface.ts b/packages/web/src/javascripts/Components/NoteView/Controller/ItemViewControllerInterface.ts similarity index 100% rename from packages/snjs/lib/Client/ItemViewControllerInterface.ts rename to packages/web/src/javascripts/Components/NoteView/Controller/ItemViewControllerInterface.ts diff --git a/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.spec.ts b/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.spec.ts new file mode 100644 index 000000000..3deb55ab9 --- /dev/null +++ b/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.spec.ts @@ -0,0 +1,83 @@ +import { WebApplication } from '@/Application/Application' +import { ContentType } from '@standardnotes/common' +import { + MutatorService, + SNComponentManager, + SNComponent, + SNTag, + ItemsClientInterface, + SNNote, +} from '@standardnotes/snjs' +import { FeatureIdentifier, NoteType } from '@standardnotes/features' +import { NoteViewController } from './NoteViewController' + +describe('note view controller', () => { + let application: WebApplication + let componentManager: SNComponentManager + + beforeEach(() => { + application = {} as jest.Mocked + application.streamItems = jest.fn() + application.getPreference = jest.fn().mockReturnValue(true) + Object.defineProperty(application, 'items', { value: {} as jest.Mocked }) + + componentManager = {} as jest.Mocked + componentManager.legacyGetDefaultEditor = jest.fn() + Object.defineProperty(application, 'componentManager', { value: componentManager }) + + const mutator = {} as jest.Mocked + mutator.createTemplateItem = jest.fn().mockReturnValue({} as SNNote) + Object.defineProperty(application, 'mutator', { value: mutator }) + }) + + it('should create notes with plaintext note type', async () => { + application.geDefaultEditorIdentifier = jest.fn().mockReturnValue(FeatureIdentifier.PlainEditor) + + const controller = new NoteViewController(application) + await controller.initialize() + + expect(application.mutator.createTemplateItem).toHaveBeenCalledWith( + ContentType.Note, + expect.objectContaining({ noteType: NoteType.Plain }), + expect.anything(), + ) + }) + + it('should create notes with markdown note type', async () => { + componentManager.legacyGetDefaultEditor = jest.fn().mockReturnValue({ + identifier: FeatureIdentifier.MarkdownProEditor, + } as SNComponent) + + componentManager.componentWithIdentifier = jest.fn().mockReturnValue({ + identifier: FeatureIdentifier.MarkdownProEditor, + } as SNComponent) + + application.geDefaultEditorIdentifier = jest.fn().mockReturnValue(FeatureIdentifier.MarkdownProEditor) + + const controller = new NoteViewController(application) + await controller.initialize() + + expect(application.mutator.createTemplateItem).toHaveBeenCalledWith( + ContentType.Note, + expect.objectContaining({ noteType: NoteType.Markdown }), + expect.anything(), + ) + }) + + it('should add tag to note if default tag is set', async () => { + application.geDefaultEditorIdentifier = jest.fn().mockReturnValue(FeatureIdentifier.PlainEditor) + + const tag = { + uuid: 'tag-uuid', + } as jest.Mocked + + application.items.findItem = jest.fn().mockReturnValue(tag) + application.items.addTagToNote = jest.fn() + + const controller = new NoteViewController(application, undefined, { tag: tag.uuid }) + await controller.initialize() + + expect(controller['defaultTag']).toEqual(tag) + expect(application.items.addTagToNote).toHaveBeenCalledWith(expect.anything(), tag, expect.anything()) + }) +}) diff --git a/packages/snjs/lib/Client/NoteViewController.ts b/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.ts similarity index 61% rename from packages/snjs/lib/Client/NoteViewController.ts rename to packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.ts index 44f2ba614..94138d18e 100644 --- a/packages/snjs/lib/Client/NoteViewController.ts +++ b/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.ts @@ -1,4 +1,5 @@ -import { NoteType } from '@standardnotes/features' +import { WebApplication } from '@/Application/Application' +import { noteTypeForEditorIdentifier } from '@standardnotes/features' import { InfoStrings } from '@standardnotes/services' import { NoteMutator, @@ -7,13 +8,15 @@ import { NoteContent, DecryptedItemInterface, PayloadEmitSource, + PrefKey, } from '@standardnotes/models' +import { UuidString } from '@standardnotes/snjs' import { removeFromArray } from '@standardnotes/utils' import { ContentType } from '@standardnotes/common' -import { UuidString } from '@Lib/Types/UuidString' -import { SNApplication } from '../Application/Application' import { ItemViewControllerInterface } from './ItemViewControllerInterface' import { TemplateNoteViewControllerOptions } from './TemplateNoteViewControllerOptions' +import { EditorSaveTimeoutDebounce } from './EditorSaveTimeoutDebounce' +import { log, LoggingDomain } from '@/Logging' export type EditorValues = { title: string @@ -23,25 +26,20 @@ export type EditorValues = { const StringEllipses = '...' const NotePreviewCharLimit = 160 -const SaveTimeoutDebounc = { - Desktop: 350, - ImmediateChange: 100, - NativeMobileWeb: 700, -} - export class NoteViewController implements ItemViewControllerInterface { public item!: SNNote public dealloced = false private innerValueChangeObservers: ((note: SNNote, source: PayloadEmitSource) => void)[] = [] - private removeStreamObserver?: () => void + private disposers: (() => void)[] = [] public isTemplateNote = false private saveTimeout?: ReturnType private defaultTagUuid: UuidString | undefined private defaultTag?: SNTag public runtimeId = `${Math.random()}` + public needsInit = true constructor( - private application: SNApplication, + private application: WebApplication, item?: SNNote, public templateNoteOptions?: TemplateNoteViewControllerOptions, ) { @@ -60,8 +58,10 @@ export class NoteViewController implements ItemViewControllerInterface { deinit(): void { this.dealloced = true - this.removeStreamObserver?.() - ;(this.removeStreamObserver as unknown) = undefined + for (const disposer of this.disposers) { + disposer() + } + this.disposers.length = 0 ;(this.application as unknown) = undefined ;(this.item as unknown) = undefined @@ -70,22 +70,30 @@ export class NoteViewController implements ItemViewControllerInterface { this.saveTimeout = undefined } - async initialize(addTagHierarchy: boolean): Promise { - if (!this.item) { - const editorIdentifier = - this.defaultTag?.preferences?.editorIdentifier || - this.application.componentManager.getDefaultEditor()?.identifier + async initialize(): Promise { + if (!this.needsInit) { + throw Error('NoteViewController already initialized') + } - const defaultEditor = editorIdentifier - ? this.application.componentManager.componentWithIdentifier(editorIdentifier) - : undefined + log(LoggingDomain.NoteView, 'Initializing NoteViewController') + + this.needsInit = false + + const addTagHierarchy = this.application.getPreference(PrefKey.NoteAddToParentFolders, true) + + if (!this.item) { + log(LoggingDomain.NoteView, 'Initializing as template note') + + const editorIdentifier = this.application.geDefaultEditorIdentifier(this.defaultTag) + + const noteType = noteTypeForEditorIdentifier(editorIdentifier) const note = this.application.mutator.createTemplateItem( ContentType.Note, { text: '', title: this.templateNoteOptions?.title || '', - noteType: defaultEditor?.noteType || NoteType.Plain, + noteType: noteType, editorIdentifier: editorIdentifier, references: [], }, @@ -119,9 +127,8 @@ export class NoteViewController implements ItemViewControllerInterface { return } - this.removeStreamObserver = this.application.streamItems( - ContentType.Note, - ({ changed, inserted, source }) => { + this.disposers.push( + this.application.streamItems(ContentType.Note, ({ changed, inserted, source }) => { if (this.dealloced) { return } @@ -137,11 +144,12 @@ export class NoteViewController implements ItemViewControllerInterface { this.item = matchingNote this.notifyObservers(matchingNote, source) } - }, + }), ) } public insertTemplatedNote(): Promise { + log(LoggingDomain.NoteView, 'Inserting template note') this.isTemplateNote = false return this.application.mutator.insertItem(this.item) } @@ -163,29 +171,55 @@ export class NoteViewController implements ItemViewControllerInterface { } } - /** - * @param bypassDebouncer Calling save will debounce by default. You can pass true to save - * immediately. - * @param isUserModified This field determines if the item will be saved as a user - * modification, thus updating the user modified date displayed in the UI - * @param dontUpdatePreviews Whether this change should update the note's plain and HTML - * preview. - * @param customMutate A custom mutator function. - */ - public async save(dto: { - editorValues: EditorValues + public async saveAndAwaitLocalPropagation(params: { + title?: string + text?: string + isUserModified: boolean bypassDebouncer?: boolean - isUserModified?: boolean - dontUpdatePreviews?: boolean + dontGeneratePreviews?: boolean + previews?: { previewPlain: string; previewHtml?: string } customMutate?: (mutator: NoteMutator) => void }): Promise { - const title = dto.editorValues.title - const text = dto.editorValues.text + if (this.needsInit) { + throw Error('NoteViewController not initialized') + } + + if (this.saveTimeout) { + clearTimeout(this.saveTimeout) + } + + const noDebounce = params.bypassDebouncer || this.application.noAccount() + + const syncDebouceMs = noDebounce + ? EditorSaveTimeoutDebounce.ImmediateChange + : this.application.isNativeMobileWeb() + ? EditorSaveTimeoutDebounce.NativeMobileWeb + : EditorSaveTimeoutDebounce.Desktop + + return new Promise((resolve) => { + this.saveTimeout = setTimeout(() => { + void this.undebouncedSave({ ...params, onLocalPropagationComplete: resolve }) + }, syncDebouceMs) + }) + } + + private async undebouncedSave(params: { + title?: string + text?: string + bypassDebouncer?: boolean + isUserModified?: boolean + dontGeneratePreviews?: boolean + previews?: { previewPlain: string; previewHtml?: string } + customMutate?: (mutator: NoteMutator) => void + onLocalPropagationComplete?: () => void + onRemoteSyncComplete?: () => void + }): Promise { + log(LoggingDomain.NoteView, 'Saving note', params) + const isTemplate = this.isTemplateNote if (typeof document !== 'undefined' && document.hidden) { void this.application.alertService.alert(InfoStrings.SavingWhileDocumentHidden) - return } if (isTemplate) { @@ -201,41 +235,37 @@ export class NoteViewController implements ItemViewControllerInterface { this.item, (mutator) => { const noteMutator = mutator as NoteMutator - if (dto.customMutate) { - dto.customMutate(noteMutator) + if (params.customMutate) { + params.customMutate(noteMutator) } - noteMutator.title = title - noteMutator.text = text - if (!dto.dontUpdatePreviews) { - const noteText = text || '' + if (params.title != undefined) { + noteMutator.title = params.title + } + + if (params.text != undefined) { + noteMutator.text = params.text + } + + if (params.previews) { + noteMutator.preview_plain = params.previews.previewPlain + noteMutator.preview_html = params.previews.previewHtml + } else if (!params.dontGeneratePreviews && params.text != undefined) { + const noteText = params.text || '' const truncate = noteText.length > NotePreviewCharLimit const substring = noteText.substring(0, NotePreviewCharLimit) const previewPlain = substring + (truncate ? StringEllipses : '') - - // eslint-disable-next-line camelcase noteMutator.preview_plain = previewPlain - // eslint-disable-next-line camelcase noteMutator.preview_html = undefined } }, - dto.isUserModified, + params.isUserModified, ) - if (this.saveTimeout) { - clearTimeout(this.saveTimeout) - } + params.onLocalPropagationComplete?.() - const noDebounce = dto.bypassDebouncer || this.application.noAccount() - - const syncDebouceMs = noDebounce - ? SaveTimeoutDebounc.ImmediateChange - : this.application.isNativeMobileWeb() - ? SaveTimeoutDebounc.NativeMobileWeb - : SaveTimeoutDebounc.Desktop - - this.saveTimeout = setTimeout(() => { - void this.application.sync.sync() - }, syncDebouceMs) + void this.application.sync.sync().then(() => { + params.onRemoteSyncComplete?.() + }) } } diff --git a/packages/snjs/lib/Client/TemplateNoteViewControllerOptions.ts b/packages/web/src/javascripts/Components/NoteView/Controller/TemplateNoteViewControllerOptions.ts similarity index 81% rename from packages/snjs/lib/Client/TemplateNoteViewControllerOptions.ts rename to packages/web/src/javascripts/Components/NoteView/Controller/TemplateNoteViewControllerOptions.ts index 1cdebefd3..898dbe32f 100644 --- a/packages/snjs/lib/Client/TemplateNoteViewControllerOptions.ts +++ b/packages/web/src/javascripts/Components/NoteView/Controller/TemplateNoteViewControllerOptions.ts @@ -1,4 +1,4 @@ -import { UuidString } from '@Lib/Types/UuidString' +import { UuidString } from '@standardnotes/snjs' export type TemplateNoteViewControllerOptions = { title?: string diff --git a/packages/web/src/javascripts/Components/NoteView/NoteView.test.ts b/packages/web/src/javascripts/Components/NoteView/NoteView.test.ts index 9509272fd..f5c1fe9af 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteView.test.ts +++ b/packages/web/src/javascripts/Components/NoteView/NoteView.test.ts @@ -8,12 +8,12 @@ import { NotesController } from '@/Controllers/NotesController' import { ApplicationEvent, ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction, - NoteViewController, SNNote, NoteType, PayloadEmitSource, } from '@standardnotes/snjs' import NoteView from './NoteView' +import { NoteViewController } from './Controller/NoteViewController' describe('NoteView', () => { let noteViewController: NoteViewController diff --git a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx index 5f286a019..85bf334af 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx +++ b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx @@ -16,30 +16,24 @@ import { ComponentArea, ComponentViewerInterface, ContentType, - EditorFontSize, - EditorLineHeight, isPayloadSourceInternalChange, isPayloadSourceRetrieved, NoteType, - NoteViewController, PayloadEmitSource, PrefKey, ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction, SNComponent, SNNote, - WebAppEvent, } from '@standardnotes/snjs' import { confirmDialog, KeyboardKey, KeyboardModifier } from '@standardnotes/ui-services' import { ChangeEventHandler, createRef, KeyboardEventHandler, RefObject } from 'react' -import { EditorEventSource } from '../../Types/EditorEventSource' -import { BlockEditor } from '../BlockEditor/BlockEditorComponent' +import { SuperEditor } from './SuperEditor/SuperEditor' import IndicatorCircle from '../IndicatorCircle/IndicatorCircle' import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContainer' import LinkedItemsButton from '../LinkedItems/LinkedItemsButton' import MobileItemsListButton from '../NoteGroupView/MobileItemsListButton' import EditingDisabledBanner from './EditingDisabledBanner' import { reloadFont } from './FontFunctions' -import { getPlaintextFontSize } from '../../Utils/getPlaintextFontSize' import NoteStatusIndicator, { NoteStatus } from './NoteStatusIndicator' import NoteViewFileDropTarget from './NoteViewFileDropTarget' import { NoteViewProps } from './NoteViewProps' @@ -48,9 +42,10 @@ import { transactionForDisassociateComponentWithCurrentNote, } from './TransactionFunctions' import { SuperEditorContentId } from '@standardnotes/blocks-editor' +import { NoteViewController } from './Controller/NoteViewController' +import { PlainEditor, PlainEditorInterface } from './PlainEditor/PlainEditor' const MinimumStatusDuration = 400 -const TextareaDebounce = 100 const NoteEditingDisabledText = 'Note editing disabled.' function sortAlphabetically(array: SNComponent[]): SNComponent[] { @@ -63,7 +58,6 @@ type State = { editorComponentViewerDidAlreadyReload?: boolean editorStateDidLoad: boolean editorTitle: string - editorText: string isDesktop?: boolean lockText: string marginResizersEnabled?: boolean @@ -75,21 +69,14 @@ type State = { spellcheck: boolean stackComponentViewers: ComponentViewerInterface[] syncTakingTooLong: boolean - /** Setting to true then false will allow the main content textarea to be destroyed - * then re-initialized. Used when reloading spellcheck status. */ - textareaUnloading: boolean - plaintextEditorFocused?: boolean - + monospaceFont?: boolean + plainEditorFocused?: boolean leftResizerWidth: number leftResizerOffset: number rightResizerWidth: number rightResizerOffset: number - monospaceFont?: boolean - lineHeight?: EditorLineHeight - fontSize?: EditorFontSize updateSavingIndicator?: boolean - editorFeatureIdentifier?: string noteType?: NoteType } @@ -98,23 +85,17 @@ class NoteView extends AbstractComponent { readonly controller!: NoteViewController private statusTimeout?: NodeJS.Timeout - private lastEditorFocusEventSource?: EditorEventSource onEditorComponentLoad?: () => void private removeTrashKeyObserver?: () => void - private removeTabObserver?: () => void private removeComponentStreamObserver?: () => void private removeComponentManagerObserver?: () => void private removeInnerNoteObserver?: () => void - private removeWebAppEventObserver: () => void - - private needsAdjustMobileCursor = false - private isAdjustingMobileCursor = false private protectionTimeoutId: ReturnType | null = null - private noteViewElementRef: RefObject private editorContentRef: RefObject + private plainEditorRef?: RefObject constructor(props: NoteViewProps) { super(props, props.application) @@ -130,18 +111,9 @@ class NoteView extends AbstractComponent { this.debounceReloadEditorComponent = debounce(this.debounceReloadEditorComponent.bind(this), 25) - this.textAreaChangeDebounceSave = debounce(this.textAreaChangeDebounceSave, TextareaDebounce) - - this.removeWebAppEventObserver = props.application.addWebEventObserver((event) => { - if (event === WebAppEvent.MobileKeyboardWillChangeFrame) { - this.scrollMobileCursorIntoViewAfterWebviewResize() - } - }) - this.state = { availableStackComponents: [], editorStateDidLoad: false, - editorText: '', editorTitle: '', isDesktop: isDesktopApplication(), lockText: NoteEditingDisabledText, @@ -152,7 +124,6 @@ class NoteView extends AbstractComponent { spellcheck: true, stackComponentViewers: [], syncTakingTooLong: false, - textareaUnloading: false, leftResizerWidth: 0, leftResizerOffset: 0, rightResizerWidth: 0, @@ -165,16 +136,6 @@ class NoteView extends AbstractComponent { this.editorContentRef = createRef() } - scrollMobileCursorIntoViewAfterWebviewResize() { - if (this.needsAdjustMobileCursor) { - this.needsAdjustMobileCursor = false - this.isAdjustingMobileCursor = true - document.getElementById('note-text-editor')?.blur() - document.getElementById('note-text-editor')?.focus() - this.isAdjustingMobileCursor = false - } - } - override deinit() { super.deinit() ;(this.controller as unknown) = undefined @@ -194,28 +155,20 @@ class NoteView extends AbstractComponent { this.clearNoteProtectionInactivityTimer() ;(this.ensureNoteIsInsertedBeforeUIAction as unknown) = undefined - this.removeWebAppEventObserver?.() - ;(this.removeWebAppEventObserver as unknown) = undefined - - this.removeTabObserver?.() - this.removeTabObserver = undefined this.onEditorComponentLoad = undefined this.statusTimeout = undefined ;(this.onPanelResizeFinish as unknown) = undefined ;(this.authorizeAndDismissProtectedWarning as unknown) = undefined ;(this.editorComponentViewerRequestsReload as unknown) = undefined - ;(this.onTextAreaChange as unknown) = undefined ;(this.onTitleEnter as unknown) = undefined ;(this.onTitleChange as unknown) = undefined - ;(this.onContentFocus as unknown) = undefined ;(this.onPanelResizeFinish as unknown) = undefined ;(this.stackComponentExpanded as unknown) = undefined ;(this.toggleStackComponent as unknown) = undefined - ;(this.onSystemEditorRef as unknown) = undefined ;(this.debounceReloadEditorComponent as unknown) = undefined - ;(this.textAreaChangeDebounceSave as unknown) = undefined ;(this.editorContentRef as unknown) = undefined + ;(this.plainEditorRef as unknown) = undefined } getState() { @@ -271,9 +224,7 @@ class NoteView extends AbstractComponent { if (this.controller.isTemplateNote) { setTimeout(() => { - if (this.controller.templateNoteOptions?.autofocusBehavior === 'editor') { - this.focusEditor() - } else { + if (this.controller.templateNoteOptions?.autofocusBehavior === 'title') { this.focusTitle() } }) @@ -296,34 +247,22 @@ class NoteView extends AbstractComponent { throw Error('Editor received changes for non-current note') } - let title = this.state.editorTitle, - text = this.state.editorText + let title = this.state.editorTitle if (isPayloadSourceRetrieved(source)) { title = note.title - text = note.text } if (!this.state.editorTitle) { title = note.title } - if (!this.state.editorText) { - text = note.text - } - if (title !== this.state.editorTitle) { this.setState({ editorTitle: title, }) } - if (text !== this.state.editorText) { - this.setState({ - editorText: text, - }) - } - if (note.locked !== this.state.noteLocked) { this.setState({ noteLocked: note.locked, @@ -334,7 +273,6 @@ class NoteView extends AbstractComponent { this.setState({ editorFeatureIdentifier: note.editorIdentifier, noteType: note.noteType, - editorText: note.text, editorTitle: note.title, }) @@ -625,36 +563,13 @@ class NoteView extends AbstractComponent { } } - onTextAreaChange: ChangeEventHandler = ({ currentTarget }) => { - const text = currentTarget.value - - this.setState({ - editorText: text, - }) - - this.textAreaChangeDebounceSave() - } - - textAreaChangeDebounceSave = () => { - log(LoggingDomain.NoteView, 'Performing save after debounce') - this.controller - .save({ - editorValues: { - title: this.state.editorTitle, - text: this.state.editorText, - }, - isUserModified: true, - }) - .catch(console.error) - } - onTitleEnter: KeyboardEventHandler = ({ key, currentTarget }) => { if (key !== KeyboardKey.Enter) { return } currentTarget.blur() - this.focusEditor() + this.plainEditorRef?.current?.focus() } onTitleChange: ChangeEventHandler = ({ currentTarget }) => { @@ -667,49 +582,18 @@ class NoteView extends AbstractComponent { }) this.controller - .save({ - editorValues: { - title: title, - text: this.state.editorText, - }, + .saveAndAwaitLocalPropagation({ + title: title, isUserModified: true, - dontUpdatePreviews: true, + dontGeneratePreviews: true, }) .catch(console.error) } - focusEditor() { - const element = document.getElementById(ElementIds.NoteTextEditor) - if (element) { - this.lastEditorFocusEventSource = EditorEventSource.Script - element.focus() - } - } - focusTitle() { document.getElementById(ElementIds.NoteTitleEditor)?.focus() } - onContentFocus = () => { - if (!this.isAdjustingMobileCursor) { - this.needsAdjustMobileCursor = true - } - if (this.lastEditorFocusEventSource) { - this.application.notifyWebEvent(WebAppEvent.EditorFocused, { eventSource: this.lastEditorFocusEventSource }) - } - - this.lastEditorFocusEventSource = undefined - this.setState({ plaintextEditorFocused: true }) - } - - onContentBlur = () => { - if (this.lastEditorFocusEventSource) { - this.application.notifyWebEvent(WebAppEvent.EditorFocused, { eventSource: this.lastEditorFocusEventSource }) - } - this.lastEditorFocusEventSource = undefined - this.setState({ plaintextEditorFocused: false }) - } - setShowProtectedOverlay(show: boolean) { this.viewControllerManager.notesController.setShowProtectedWarning(show) } @@ -737,13 +621,11 @@ class NoteView extends AbstractComponent { this.performNoteDeletion(this.note) } else { this.controller - .save({ - editorValues: { - title: this.state.editorTitle, - text: this.state.editorText, - }, + .saveAndAwaitLocalPropagation({ + title: this.state.editorTitle, bypassDebouncer: true, - dontUpdatePreviews: true, + dontGeneratePreviews: true, + isUserModified: true, customMutate: (mutator) => { mutator.trashed = true }, @@ -773,15 +655,9 @@ class NoteView extends AbstractComponent { async reloadSpellcheck() { const spellcheck = this.viewControllerManager.notesController.getSpellcheckStateForNote(this.note) - if (spellcheck !== this.state.spellcheck) { - this.setState({ textareaUnloading: true }) - this.setState({ textareaUnloading: false }) reloadFont(this.state.monospaceFont) - - this.setState({ - spellcheck, - }) + this.setState({ spellcheck }) } } @@ -797,10 +673,6 @@ class NoteView extends AbstractComponent { PrefDefaults[PrefKey.EditorResizersEnabled], ) - const lineHeight = this.application.getPreference(PrefKey.EditorLineHeight, PrefDefaults[PrefKey.EditorLineHeight]) - - const fontSize = this.application.getPreference(PrefKey.EditorFontSize, PrefDefaults[PrefKey.EditorFontSize]) - const updateSavingIndicator = this.application.getPreference( PrefKey.UpdateSavingStatusIndicator, PrefDefaults[PrefKey.UpdateSavingStatusIndicator], @@ -811,8 +683,7 @@ class NoteView extends AbstractComponent { this.setState({ monospaceFont, marginResizersEnabled, - lineHeight, - fontSize, + updateSavingIndicator, }) @@ -904,82 +775,20 @@ class NoteView extends AbstractComponent { }) } - onSystemEditorRef = (ref: HTMLTextAreaElement | null) => { - if (this.removeTabObserver || !ref) { - return - } - - log(LoggingDomain.NoteView, 'On system editor ref') - - /** - * Insert 4 spaces when a tab key is pressed, - * only used when inside of the text editor. - * If the shift key is pressed first, this event is - * not fired. - */ - const editor = document.getElementById(ElementIds.NoteTextEditor) as HTMLInputElement - - if (!editor) { - console.error('Editor is not yet mounted; unable to add tab observer.') - return - } - - this.removeTabObserver = this.application.io.addKeyObserver({ - element: editor, - key: KeyboardKey.Tab, - onKeyDown: (event) => { - if (document.hidden || this.note.locked || event.shiftKey) { - return - } - event.preventDefault() - /** Using document.execCommand gives us undo support */ - const insertSuccessful = document.execCommand('insertText', false, '\t') - if (!insertSuccessful) { - /** document.execCommand works great on Chrome/Safari but not Firefox */ - const start = editor.selectionStart || 0 - const end = editor.selectionEnd || 0 - const spaces = ' ' - /** Insert 4 spaces */ - editor.value = editor.value.substring(0, start) + spaces + editor.value.substring(end) - /** Place cursor 4 spaces away from where the tab key was pressed */ - editor.selectionStart = editor.selectionEnd = start + 4 - } - this.setState({ - editorText: editor.value, - }) - - this.controller - .save({ - editorValues: { - title: this.state.editorTitle, - text: this.state.editorText, - }, - bypassDebouncer: true, - }) - .catch(console.error) - }, - }) - - const observer = new MutationObserver((records) => { - for (const record of records) { - record.removedNodes.forEach((node) => { - if (node === editor) { - this.removeTabObserver?.() - this.removeTabObserver = undefined - } - }) - } - }) - - observer.observe(editor.parentElement as HTMLElement, { childList: true }) - } - ensureNoteIsInsertedBeforeUIAction = async () => { if (this.controller.isTemplateNote) { await this.controller.insertTemplatedNote() } } + onPlainFocus = () => { + this.setState({ plainEditorFocused: true }) + } + + onPlainBlur = () => { + this.setState({ plainEditorFocused: false }) + } + override render() { if (this.controller.dealloced) { return null @@ -996,12 +805,12 @@ class NoteView extends AbstractComponent { ) } - const renderHeaderOptions = isMobileScreen() ? !this.state.plaintextEditorFocused : true + const renderHeaderOptions = isMobileScreen() ? !this.state.plainEditorFocused : true const editorMode = - this.note.noteType === NoteType.Blocks - ? 'blocks' - : this.state.editorStateDidLoad && !this.state.editorComponentViewer && !this.state.textareaUnloading + this.note.noteType === NoteType.Super + ? 'super' + : this.state.editorStateDidLoad && !this.state.editorComponentViewer ? 'plain' : this.state.editorComponentViewer ? 'component' @@ -1095,7 +904,7 @@ class NoteView extends AbstractComponent {
)} - {editorMode !== 'blocks' && ( + {editorMode !== 'super' && ( )} @@ -1103,7 +912,7 @@ class NoteView extends AbstractComponent {
{this.state.marginResizersEnabled && this.editorContentRef.current ? ( @@ -1133,34 +942,26 @@ class NoteView extends AbstractComponent { )} {editorMode === 'plain' && ( - + )} - {editorMode === 'blocks' && ( + {editorMode === 'super' && (
-
)} diff --git a/packages/web/src/javascripts/Components/NoteView/NoteViewProps.ts b/packages/web/src/javascripts/Components/NoteView/NoteViewProps.ts index 114ad3d0c..b39885dca 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteViewProps.ts +++ b/packages/web/src/javascripts/Components/NoteView/NoteViewProps.ts @@ -1,6 +1,5 @@ -import { NoteViewController } from '@standardnotes/snjs' - import { WebApplication } from '@/Application/Application' +import { NoteViewController } from './Controller/NoteViewController' export interface NoteViewProps { application: WebApplication diff --git a/packages/web/src/javascripts/Components/NoteView/PlainEditor/PlainEditor.tsx b/packages/web/src/javascripts/Components/NoteView/PlainEditor/PlainEditor.tsx new file mode 100644 index 000000000..6d7b53b06 --- /dev/null +++ b/packages/web/src/javascripts/Components/NoteView/PlainEditor/PlainEditor.tsx @@ -0,0 +1,253 @@ +import { WebApplication } from '@/Application/Application' +import { usePrevious } from '@/Components/ContentListView/Calendar/usePrevious' +import { ElementIds } from '@/Constants/ElementIDs' +import { PrefDefaults } from '@/Constants/PrefDefaults' +import { log, LoggingDomain } from '@/Logging' +import { Disposer } from '@/Types/Disposer' +import { EditorEventSource } from '@/Types/EditorEventSource' +import { classNames } from '@/Utils/ConcatenateClassNames' +import { getPlaintextFontSize } from '@/Utils/getPlaintextFontSize' +import { + ApplicationEvent, + EditorFontSize, + EditorLineHeight, + isPayloadSourceRetrieved, + PrefKey, + WebAppEvent, +} from '@standardnotes/snjs' +import { KeyboardKey } from '@standardnotes/ui-services' +import { ChangeEventHandler, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react' +import { NoteViewController } from '../Controller/NoteViewController' + +type Props = { + application: WebApplication + spellcheck: boolean + controller: NoteViewController + locked: boolean + onFocus: () => void + onBlur: () => void +} + +export type PlainEditorInterface = { + focus: () => void +} + +export const PlainEditor = forwardRef( + ({ application, spellcheck, controller, locked, onFocus, onBlur }: Props, ref) => { + const [editorText, setEditorText] = useState() + const [textareaUnloading, setTextareaUnloading] = useState(false) + const [lineHeight, setLineHeight] = useState() + const [fontSize, setFontSize] = useState() + const previousSpellcheck = usePrevious(spellcheck) + + const lastEditorFocusEventSource = useRef() + const needsAdjustMobileCursor = useRef(false) + const isAdjustingMobileCursor = useRef(false) + const note = useRef(controller.item) + + const tabObserverDisposer = useRef() + + useImperativeHandle(ref, () => ({ + focus() { + focusEditor() + }, + })) + + useEffect(() => { + const disposer = controller.addNoteInnerValueChangeObserver((updatedNote, source) => { + if (updatedNote.uuid !== note.current.uuid) { + throw Error('Editor received changes for non-current note') + } + + if ( + isPayloadSourceRetrieved(source) || + editorText == undefined || + updatedNote.editorIdentifier !== note.current.editorIdentifier || + updatedNote.noteType !== note.current.noteType + ) { + setEditorText(updatedNote.text) + } + + note.current = updatedNote + }) + + return disposer + }, [controller, editorText, controller.item.uuid, controller.item.editorIdentifier, controller.item.noteType]) + + const onTextAreaChange: ChangeEventHandler = ({ currentTarget }) => { + const text = currentTarget.value + + setEditorText(text) + + void controller.saveAndAwaitLocalPropagation({ text: text, isUserModified: true }) + } + + const onContentFocus = useCallback(() => { + if (!isAdjustingMobileCursor.current) { + needsAdjustMobileCursor.current = true + } + + if (lastEditorFocusEventSource.current) { + application.notifyWebEvent(WebAppEvent.EditorFocused, { eventSource: lastEditorFocusEventSource }) + } + + lastEditorFocusEventSource.current = undefined + onFocus() + }, [application, isAdjustingMobileCursor, lastEditorFocusEventSource, onFocus]) + + const onContentBlur = useCallback(() => { + if (lastEditorFocusEventSource.current) { + application.notifyWebEvent(WebAppEvent.EditorFocused, { eventSource: lastEditorFocusEventSource }) + } + lastEditorFocusEventSource.current = undefined + onBlur() + }, [application, lastEditorFocusEventSource, onBlur]) + + const scrollMobileCursorIntoViewAfterWebviewResize = useCallback(() => { + if (needsAdjustMobileCursor.current) { + needsAdjustMobileCursor.current = false + isAdjustingMobileCursor.current = true + document.getElementById('note-text-editor')?.blur() + document.getElementById('note-text-editor')?.focus() + isAdjustingMobileCursor.current = false + } + }, [needsAdjustMobileCursor]) + + useEffect(() => { + const disposer = application.addWebEventObserver((event) => { + if (event === WebAppEvent.MobileKeyboardWillChangeFrame) { + scrollMobileCursorIntoViewAfterWebviewResize() + } + }) + return disposer + }, [application, scrollMobileCursorIntoViewAfterWebviewResize]) + + const focusEditor = useCallback(() => { + const element = document.getElementById(ElementIds.NoteTextEditor) + if (element) { + lastEditorFocusEventSource.current = EditorEventSource.Script + element.focus() + } + }, []) + + useEffect(() => { + if (controller.isTemplateNote && controller.templateNoteOptions?.autofocusBehavior === 'editor') { + setTimeout(() => { + focusEditor() + }) + } + }, [controller, focusEditor]) + + const reloadPreferences = useCallback(() => { + const lineHeight = application.getPreference(PrefKey.EditorLineHeight, PrefDefaults[PrefKey.EditorLineHeight]) + const fontSize = application.getPreference(PrefKey.EditorFontSize, PrefDefaults[PrefKey.EditorFontSize]) + + setLineHeight(lineHeight) + setFontSize(fontSize) + }, [application]) + + useEffect(() => { + reloadPreferences() + + return application.addSingleEventObserver(ApplicationEvent.PreferencesChanged, async () => { + reloadPreferences() + }) + }, [reloadPreferences, application]) + + useEffect(() => { + if (spellcheck !== previousSpellcheck) { + setTextareaUnloading(true) + setTimeout(() => { + setTextareaUnloading(false) + }, 0) + } + }, [spellcheck, previousSpellcheck]) + + const onRef = (ref: HTMLTextAreaElement | null) => { + if (tabObserverDisposer.current || !ref) { + return + } + + log(LoggingDomain.NoteView, 'On system editor ref') + + /** + * Insert 4 spaces when a tab key is pressed, only used when inside of the text editor. + * If the shift key is pressed first, this event is not fired. + */ + const editor = document.getElementById(ElementIds.NoteTextEditor) as HTMLInputElement + + if (!editor) { + console.error('Editor is not yet mounted; unable to add tab observer.') + return + } + + tabObserverDisposer.current = application.io.addKeyObserver({ + element: editor, + key: KeyboardKey.Tab, + onKeyDown: (event) => { + if (document.hidden || note.current.locked || event.shiftKey) { + return + } + event.preventDefault() + /** Using document.execCommand gives us undo support */ + const insertSuccessful = document.execCommand('insertText', false, '\t') + if (!insertSuccessful) { + /** document.execCommand works great on Chrome/Safari but not Firefox */ + const start = editor.selectionStart || 0 + const end = editor.selectionEnd || 0 + const spaces = ' ' + /** Insert 4 spaces */ + editor.value = editor.value.substring(0, start) + spaces + editor.value.substring(end) + /** Place cursor 4 spaces away from where the tab key was pressed */ + editor.selectionStart = editor.selectionEnd = start + 4 + } + + setEditorText(editor.value) + + void controller.saveAndAwaitLocalPropagation({ + text: editor.value, + bypassDebouncer: true, + isUserModified: true, + }) + }, + }) + + const observer = new MutationObserver((records) => { + for (const record of records) { + record.removedNodes.forEach((node) => { + if (node === editor) { + tabObserverDisposer.current?.() + tabObserverDisposer.current = undefined + } + }) + } + }) + + observer.observe(editor.parentElement as HTMLElement, { childList: true }) + } + + if (textareaUnloading) { + return null + } + + return ( + + ) + }, +) diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/AutoLinkPlugin/AutoLinkPlugin.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/AutoLinkPlugin/AutoLinkPlugin.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/AutoLinkPlugin/AutoLinkPlugin.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/AutoLinkPlugin/AutoLinkPlugin.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/BlockPickerMenuItem.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/BlockPickerMenuItem.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/BlockPickerMenuItem.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/BlockPickerMenuItem.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/BlockPickerOption.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/BlockPickerOption.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/BlockPickerOption.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/BlockPickerOption.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/BlockPickerPlugin.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/BlockPickerPlugin.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/BlockPickerPlugin.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/BlockPickerPlugin.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/Alignment.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/Alignment.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/Alignment.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/Alignment.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/BulletedList.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/BulletedList.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/BulletedList.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/BulletedList.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/Checklist.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/Checklist.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/Checklist.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/Checklist.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/Code.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/Code.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/Code.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/Code.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/Collapsible.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/Collapsible.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/Collapsible.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/Collapsible.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/DateTime.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/DateTime.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/DateTime.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/DateTime.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/Divider.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/Divider.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/Divider.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/Divider.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/Embeds.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/Embeds.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/Embeds.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/Embeds.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/Headings.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/Headings.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/Headings.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/Headings.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/NumberedList.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/NumberedList.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/NumberedList.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/NumberedList.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/Paragraph.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/Paragraph.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/Paragraph.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/Paragraph.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/Quote.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/Quote.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/Quote.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/Quote.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/Table.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/Table.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/BlockPickerPlugin/Blocks/Table.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/BlockPickerPlugin/Blocks/Table.tsx diff --git a/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ChangeContentCallback/ChangeContentCallback.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ChangeContentCallback/ChangeContentCallback.tsx new file mode 100644 index 000000000..92c369155 --- /dev/null +++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ChangeContentCallback/ChangeContentCallback.tsx @@ -0,0 +1,26 @@ +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { useEffect } from 'react' + +export type ChangeEditorFunction = (jsonContent: string) => void +type ChangeEditorFunctionProvider = (changeEditorFunction: ChangeEditorFunction) => void + +export function ChangeContentCallbackPlugin({ + providerCallback, +}: { + providerCallback: ChangeEditorFunctionProvider +}): JSX.Element | null { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + const changeContents: ChangeEditorFunction = (jsonContent: string) => { + editor.update(() => { + const editorState = editor.parseEditorState(jsonContent) + editor.setEditorState(editorState) + }) + } + + providerCallback(changeContents) + }, [editor, providerCallback]) + + return null +} diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/ClassNames.ts b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ClassNames.ts similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/ClassNames.ts rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ClassNames.ts diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/Commands.ts b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/Commands.ts similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/Commands.ts rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/Commands.ts diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/DateTimePlugin/DateTimePlugin.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/DateTimePlugin/DateTimePlugin.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/DateTimePlugin/DateTimePlugin.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/DateTimePlugin/DateTimePlugin.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/EncryptedFilePlugin/FilePlugin.ts b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/FilePlugin.ts similarity index 96% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/EncryptedFilePlugin/FilePlugin.ts rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/FilePlugin.ts index 01073b4ed..65209b582 100644 --- a/packages/web/src/javascripts/Components/BlockEditor/Plugins/EncryptedFilePlugin/FilePlugin.ts +++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/FilePlugin.ts @@ -1,4 +1,4 @@ -import { INSERT_FILE_COMMAND } from './../Commands' +import { INSERT_FILE_COMMAND } from '../Commands' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useEffect } from 'react' diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/EncryptedFilePlugin/Nodes/FileComponent.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/Nodes/FileComponent.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/EncryptedFilePlugin/Nodes/FileComponent.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/Nodes/FileComponent.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/EncryptedFilePlugin/Nodes/FileNode.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/Nodes/FileNode.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/EncryptedFilePlugin/Nodes/FileNode.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/Nodes/FileNode.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/EncryptedFilePlugin/Nodes/FileUtils.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/Nodes/FileUtils.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/EncryptedFilePlugin/Nodes/FileUtils.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/Nodes/FileUtils.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/EncryptedFilePlugin/Nodes/SerializedFileNode.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/Nodes/SerializedFileNode.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/EncryptedFilePlugin/Nodes/SerializedFileNode.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/EncryptedFilePlugin/Nodes/SerializedFileNode.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/ImportPlugin/ImportPlugin.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ImportPlugin/ImportPlugin.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/ImportPlugin/ImportPlugin.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ImportPlugin/ImportPlugin.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemBubblePlugin/ItemBubblePlugin.ts b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemBubblePlugin/ItemBubblePlugin.ts similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemBubblePlugin/ItemBubblePlugin.ts rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemBubblePlugin/ItemBubblePlugin.ts diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemBubblePlugin/Nodes/BubbleComponent.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemBubblePlugin/Nodes/BubbleComponent.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemBubblePlugin/Nodes/BubbleComponent.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemBubblePlugin/Nodes/BubbleComponent.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemBubblePlugin/Nodes/BubbleNode.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemBubblePlugin/Nodes/BubbleNode.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemBubblePlugin/Nodes/BubbleNode.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemBubblePlugin/Nodes/BubbleNode.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemBubblePlugin/Nodes/BubbleUtils.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemBubblePlugin/Nodes/BubbleUtils.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemBubblePlugin/Nodes/BubbleUtils.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemBubblePlugin/Nodes/BubbleUtils.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemBubblePlugin/Nodes/SerializedBubbleNode.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemBubblePlugin/Nodes/SerializedBubbleNode.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemBubblePlugin/Nodes/SerializedBubbleNode.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemBubblePlugin/Nodes/SerializedBubbleNode.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemNodeInterface.ts b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemNodeInterface.ts similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemNodeInterface.ts rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemNodeInterface.ts diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemSelectionPlugin/ItemOption.ts b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemSelectionPlugin/ItemOption.ts similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemSelectionPlugin/ItemOption.ts rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemSelectionPlugin/ItemOption.ts diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemSelectionPlugin/ItemSelectionItemComponent.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemSelectionPlugin/ItemSelectionItemComponent.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemSelectionPlugin/ItemSelectionItemComponent.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemSelectionPlugin/ItemSelectionItemComponent.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemSelectionPlugin/ItemSelectionPlugin.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemSelectionPlugin/ItemSelectionPlugin.tsx similarity index 98% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemSelectionPlugin/ItemSelectionPlugin.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemSelectionPlugin/ItemSelectionPlugin.tsx index da39c9754..54dd7a038 100644 --- a/packages/web/src/javascripts/Components/BlockEditor/Plugins/ItemSelectionPlugin/ItemSelectionPlugin.tsx +++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/ItemSelectionPlugin/ItemSelectionPlugin.tsx @@ -9,7 +9,7 @@ import { ContentType, SNNote } from '@standardnotes/snjs' import { getLinkingSearchResults } from '@/Utils/Items/Search/getSearchResults' import Popover from '@/Components/Popover/Popover' import { INSERT_BUBBLE_COMMAND, INSERT_FILE_COMMAND } from '../Commands' -import { useLinkingController } from '../../../../Controllers/LinkingControllerProvider' +import { useLinkingController } from '../../../../../Controllers/LinkingControllerProvider' import { PopoverClassNames } from '../ClassNames' type Props = { diff --git a/packages/web/src/javascripts/Components/BlockEditor/Plugins/NodeObserverPlugin/NodeObserverPlugin.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/NodeObserverPlugin/NodeObserverPlugin.tsx similarity index 100% rename from packages/web/src/javascripts/Components/BlockEditor/Plugins/NodeObserverPlugin/NodeObserverPlugin.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/NodeObserverPlugin/NodeObserverPlugin.tsx diff --git a/packages/web/src/javascripts/Components/BlockEditor/BlockEditorComponent.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperEditor.tsx similarity index 55% rename from packages/web/src/javascripts/Components/BlockEditor/BlockEditorComponent.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperEditor.tsx index 4d932cf74..0769e47ca 100644 --- a/packages/web/src/javascripts/Components/BlockEditor/BlockEditorComponent.tsx +++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperEditor.tsx @@ -1,7 +1,6 @@ import { WebApplication } from '@/Application/Application' -import { SNNote } from '@standardnotes/snjs' -import { FunctionComponent, useCallback, useRef } from 'react' -import { BlockEditorController } from './BlockEditorController' +import { isPayloadSourceRetrieved } from '@standardnotes/snjs' +import { FunctionComponent, useCallback, useEffect, useRef } from 'react' import { BlocksEditor, BlocksEditorComposer } from '@standardnotes/blocks-editor' import { ItemSelectionPlugin } from './Plugins/ItemSelectionPlugin/ItemSelectionPlugin' import { FileNode } from './Plugins/EncryptedFilePlugin/Nodes/FileNode' @@ -9,7 +8,7 @@ import FilePlugin from './Plugins/EncryptedFilePlugin/FilePlugin' import BlockPickerMenuPlugin from './Plugins/BlockPickerPlugin/BlockPickerPlugin' import { ErrorBoundary } from '@/Utils/ErrorBoundary' import { LinkingController } from '@/Controllers/LinkingController' -import LinkingControllerProvider from '../../Controllers/LinkingControllerProvider' +import LinkingControllerProvider from '../../../Controllers/LinkingControllerProvider' import { BubbleNode } from './Plugins/ItemBubblePlugin/Nodes/BubbleNode' import ItemBubblePlugin from './Plugins/ItemBubblePlugin/ItemBubblePlugin' import { NodeObserverPlugin } from './Plugins/NodeObserverPlugin/NodeObserverPlugin' @@ -17,29 +16,48 @@ import { FilesController } from '@/Controllers/FilesController' import FilesControllerProvider from '@/Controllers/FilesControllerProvider' import DatetimePlugin from './Plugins/DateTimePlugin/DateTimePlugin' import AutoLinkPlugin from './Plugins/AutoLinkPlugin/AutoLinkPlugin' +import { NoteViewController } from '../Controller/NoteViewController' +import { + ChangeContentCallbackPlugin, + ChangeEditorFunction, +} from './Plugins/ChangeContentCallback/ChangeContentCallback' const NotePreviewCharLimit = 160 type Props = { application: WebApplication - note: SNNote + controller: NoteViewController linkingController: LinkingController filesController: FilesController spellcheck: boolean } -export const BlockEditor: FunctionComponent = ({ - note, +export const SuperEditor: FunctionComponent = ({ application, linkingController, filesController, spellcheck, + controller, }) => { - const controller = useRef(new BlockEditorController(note, application)) + const note = useRef(controller.item) + const changeEditorFunction = useRef() + const ignoreNextChange = useRef(false) const handleChange = useCallback( - (value: string, preview: string) => { - void controller.current.save({ text: value, previewPlain: preview, previewHtml: undefined }) + async (value: string, preview: string) => { + if (ignoreNextChange.current === true) { + ignoreNextChange.current = false + return + } + + void controller.saveAndAwaitLocalPropagation({ + text: value, + isUserModified: true, + previews: { + previewPlain: preview, + previewHtml: undefined, + }, + }) }, [controller], ) @@ -54,24 +72,49 @@ export const BlockEditor: FunctionComponent = ({ [linkingController, application], ) + useEffect(() => { + const disposer = controller.addNoteInnerValueChangeObserver((updatedNote, source) => { + if (updatedNote.uuid !== note.current.uuid) { + throw Error('Editor received changes for non-current note') + } + + if (isPayloadSourceRetrieved(source)) { + ignoreNextChange.current = true + changeEditorFunction.current?.(updatedNote.text) + } + + note.current = updatedNote + }) + + return disposer + }, [controller, controller.item.uuid]) + return (
- + - + + (changeEditorFunction.current = callback)} + /> diff --git a/packages/web/src/javascripts/Components/BlockEditor/SuperNoteImporter.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperNoteImporter.tsx similarity index 77% rename from packages/web/src/javascripts/Components/BlockEditor/SuperNoteImporter.tsx rename to packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperNoteImporter.tsx index 3636197f9..d207542a7 100644 --- a/packages/web/src/javascripts/Components/BlockEditor/SuperNoteImporter.tsx +++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperNoteImporter.tsx @@ -1,7 +1,6 @@ import { WebApplication } from '@/Application/Application' import { NoteType, SNNote } from '@standardnotes/snjs' -import { FunctionComponent, useCallback, useState } from 'react' -import { BlockEditorController } from './BlockEditorController' +import { FunctionComponent, useCallback, useEffect, useState } from 'react' import { BlocksEditor, BlocksEditorComposer } from '@standardnotes/blocks-editor' import { ErrorBoundary } from '@/Utils/ErrorBoundary' import ModalDialog from '@/Components/Shared/ModalDialog' @@ -10,6 +9,7 @@ import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription' import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel' import Button from '@/Components/Button/Button' import ImportPlugin from './Plugins/ImportPlugin/ImportPlugin' +import { NoteViewController } from '../Controller/NoteViewController' export function spaceSeparatedStrings(...strings: string[]): string { return strings.join(' ') @@ -36,12 +36,31 @@ export const SuperNoteImporter: FunctionComponent = ({ note, application, setLastValue({ text: value, previewPlain: preview }) }, []) - const confirmConvert = useCallback(() => { - const controller = new BlockEditorController(note, application) - void controller.save({ text: lastValue.text, previewPlain: lastValue.previewPlain, previewHtml: undefined }) + const performConvert = useCallback( + async (text: string, previewPlain: string) => { + const controller = new NoteViewController(application, note) + await controller.initialize() + await controller.saveAndAwaitLocalPropagation({ + text: text, + previews: { previewPlain: previewPlain, previewHtml: undefined }, + isUserModified: true, + bypassDebouncer: true, + }) + }, + [application, note], + ) + + const confirmConvert = useCallback(async () => { + await performConvert(lastValue.text, lastValue.previewPlain) closeDialog() onConvertComplete() - }, [closeDialog, application, lastValue, note, onConvertComplete]) + }, [closeDialog, performConvert, onConvertComplete, lastValue]) + + useEffect(() => { + if (note.text.length === 0) { + void confirmConvert() + } + }, [note, confirmConvert]) const convertAsIs = useCallback(async () => { const confirmed = await application.alertService.confirm( @@ -56,11 +75,11 @@ export const SuperNoteImporter: FunctionComponent = ({ note, application, return } - const controller = new BlockEditorController(note, application) - void controller.save({ text: note.text, previewPlain: note.preview_plain, previewHtml: undefined }) + await performConvert(note.text, note.preview_plain) + closeDialog() onConvertComplete() - }, [closeDialog, application, note, onConvertComplete]) + }, [closeDialog, application, note, onConvertComplete, performConvert]) return ( @@ -77,6 +96,7 @@ export const SuperNoteImporter: FunctionComponent = ({ note, application, = { - [FeatureTrunkName.Blocks]: isDev && true, + [FeatureTrunkName.Super]: isDev && true, } export function featureTrunkEnabled(trunk: FeatureTrunkName): boolean { diff --git a/packages/web/src/javascripts/Logging.ts b/packages/web/src/javascripts/Logging.ts index 1c3ed7241..2f9d01e15 100644 --- a/packages/web/src/javascripts/Logging.ts +++ b/packages/web/src/javascripts/Logging.ts @@ -20,7 +20,7 @@ const LoggingStatus: Record = { [LoggingDomain.Viewport]: false, [LoggingDomain.Selection]: false, [LoggingDomain.BlockEditor]: false, - [LoggingDomain.Purchasing]: true, + [LoggingDomain.Purchasing]: false, } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/web/src/javascripts/Utils/DropdownItemsForEditors.ts b/packages/web/src/javascripts/Utils/DropdownItemsForEditors.ts index ab9fb49c9..ad1ac2a26 100644 --- a/packages/web/src/javascripts/Utils/DropdownItemsForEditors.ts +++ b/packages/web/src/javascripts/Utils/DropdownItemsForEditors.ts @@ -1,23 +1,34 @@ -import { ComponentArea, FeatureIdentifier } from '@standardnotes/features' -import { DropdownItem } from '@/Components/Dropdown/DropdownItem' +import { FeatureIdentifier } from '@standardnotes/snjs' +import { ComponentArea, NoteType } from '@standardnotes/features' import { WebApplication } from '@/Application/Application' -import { BLOCKS_EDITOR_NAME, PLAIN_EDITOR_NAME } from '@/Constants/Constants' -import { featureTrunkEnabled, FeatureTrunkName } from '@/FeatureTrunk' +import { PlainEditorMetadata, SuperEditorMetadata } from '@/Constants/Constants' import { getIconAndTintForNoteType } from './Items/Icons/getIconAndTintForNoteType' - -export const PlainEditorType = 'plain-editor' -export const BlocksType = 'blocks-editor' +import { DropdownItem } from '@/Components/Dropdown/DropdownItem' export type EditorOption = DropdownItem & { - value: FeatureIdentifier | typeof PlainEditorType | typeof BlocksType + value: FeatureIdentifier } -export function getDropdownItemsForAllEditors(application: WebApplication) { +export function noteTypeForEditorOptionValue(value: EditorOption['value'], application: WebApplication): NoteType { + if (value === FeatureIdentifier.PlainEditor) { + return NoteType.Plain + } else if (value === FeatureIdentifier.SuperEditor) { + return NoteType.Super + } + + const matchingEditor = application.componentManager + .componentsForArea(ComponentArea.Editor) + .find((editor) => editor.identifier === value) + + return matchingEditor ? matchingEditor.noteType : NoteType.Unknown +} + +export function getDropdownItemsForAllEditors(application: WebApplication): EditorOption[] { const plaintextOption: EditorOption = { - icon: 'plain-text', - iconClassName: 'text-accessory-tint-1', - label: PLAIN_EDITOR_NAME, - value: PlainEditorType, + icon: PlainEditorMetadata.icon, + iconClassName: PlainEditorMetadata.iconClassName, + label: PlainEditorMetadata.name, + value: FeatureIdentifier.PlainEditor, } const options = application.componentManager.componentsForArea(ComponentArea.Editor).map((editor): EditorOption => { @@ -34,12 +45,12 @@ export function getDropdownItemsForAllEditors(application: WebApplication) { options.push(plaintextOption) - if (featureTrunkEnabled(FeatureTrunkName.Blocks)) { + if (application.features.isExperimentalFeatureEnabled(FeatureIdentifier.SuperEditor)) { options.push({ - icon: 'dashboard', - iconClassName: 'text-accessory-tint-1', - label: BLOCKS_EDITOR_NAME, - value: BlocksType, + icon: SuperEditorMetadata.icon, + iconClassName: SuperEditorMetadata.iconClassName, + label: SuperEditorMetadata.name, + value: FeatureIdentifier.SuperEditor, }) } diff --git a/packages/web/src/javascripts/Utils/Items/Icons/getIconAndTintForNoteType.ts b/packages/web/src/javascripts/Utils/Items/Icons/getIconAndTintForNoteType.ts index e4d45225e..f37e0365d 100644 --- a/packages/web/src/javascripts/Utils/Items/Icons/getIconAndTintForNoteType.ts +++ b/packages/web/src/javascripts/Utils/Items/Icons/getIconAndTintForNoteType.ts @@ -1,3 +1,4 @@ +import { PlainEditorMetadata, SuperEditorMetadata } from '@/Constants/Constants' import { NoteType } from '@standardnotes/features' import { IconType } from '@standardnotes/models' @@ -15,7 +16,9 @@ export function getIconAndTintForNoteType(noteType?: NoteType): [IconType, numbe return ['tasks', 3] case NoteType.Code: return ['code', 4] + case NoteType.Super: + return [SuperEditorMetadata.icon, SuperEditorMetadata.iconTintNumber] default: - return ['plain-text', 1] + return [PlainEditorMetadata.icon, PlainEditorMetadata.iconTintNumber] } } diff --git a/packages/web/src/javascripts/Utils/createEditorMenuGroups.ts b/packages/web/src/javascripts/Utils/createEditorMenuGroups.ts index 4b06324f1..2daf70f4c 100644 --- a/packages/web/src/javascripts/Utils/createEditorMenuGroups.ts +++ b/packages/web/src/javascripts/Utils/createEditorMenuGroups.ts @@ -1,4 +1,3 @@ -import { featureTrunkEnabled, FeatureTrunkName } from '@/FeatureTrunk' import { WebApplication } from '@/Application/Application' import { ContentType, @@ -8,10 +7,11 @@ import { FeatureDescription, GetFeatures, NoteType, + FeatureIdentifier, } from '@standardnotes/snjs' import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup' import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem' -import { BLOCKS_EDITOR_NAME, PLAIN_EDITOR_NAME } from '@/Constants/Constants' +import { PlainEditorMetadata, SuperEditorMetadata } from '@/Constants/Constants' type NoteTypeToEditorRowsMap = Record @@ -72,7 +72,7 @@ const insertInstalledComponentsInMap = ( }) } -const createGroupsFromMap = (map: NoteTypeToEditorRowsMap): EditorMenuGroup[] => { +const createGroupsFromMap = (map: NoteTypeToEditorRowsMap, application: WebApplication): EditorMenuGroup[] => { const groups: EditorMenuGroup[] = [ { icon: 'plain-text', @@ -124,12 +124,12 @@ const createGroupsFromMap = (map: NoteTypeToEditorRowsMap): EditorMenuGroup[] => }, ] - if (featureTrunkEnabled(FeatureTrunkName.Blocks)) { + if (application.features.isExperimentalFeatureEnabled(FeatureIdentifier.SuperEditor)) { groups.splice(1, 0, { - icon: 'file-doc', - iconClassName: 'text-accessory-tint-4', - title: BLOCKS_EDITOR_NAME, - items: map[NoteType.Blocks], + icon: SuperEditorMetadata.icon, + iconClassName: SuperEditorMetadata.iconClassName, + title: SuperEditorMetadata.name, + items: map[NoteType.Super], featured: true, }) } @@ -137,16 +137,16 @@ const createGroupsFromMap = (map: NoteTypeToEditorRowsMap): EditorMenuGroup[] => return groups } -const createBaselineMap = (): NoteTypeToEditorRowsMap => { +const createBaselineMap = (application: WebApplication): NoteTypeToEditorRowsMap => { const map: NoteTypeToEditorRowsMap = { [NoteType.Plain]: [ { - name: PLAIN_EDITOR_NAME, + name: PlainEditorMetadata.name, isEntitled: true, noteType: NoteType.Plain, }, ], - [NoteType.Blocks]: [], + [NoteType.Super]: [], [NoteType.RichText]: [], [NoteType.Markdown]: [], [NoteType.Task]: [], @@ -156,11 +156,11 @@ const createBaselineMap = (): NoteTypeToEditorRowsMap => { [NoteType.Unknown]: [], } - if (featureTrunkEnabled(FeatureTrunkName.Blocks)) { - map[NoteType.Blocks].push({ - name: BLOCKS_EDITOR_NAME, + if (application.features.isExperimentalFeatureEnabled(FeatureIdentifier.SuperEditor)) { + map[NoteType.Super].push({ + name: SuperEditorMetadata.name, isEntitled: true, - noteType: NoteType.Blocks, + noteType: NoteType.Super, }) } @@ -168,11 +168,11 @@ const createBaselineMap = (): NoteTypeToEditorRowsMap => { } export const createEditorMenuGroups = (application: WebApplication, components: SNComponent[]) => { - const map = createBaselineMap() + const map = createBaselineMap(application) insertNonInstalledNativeComponentsInMap(map, components, application) insertInstalledComponentsInMap(map, components, application) - return createGroupsFromMap(map) + return createGroupsFromMap(map, application) } diff --git a/packages/web/src/javascripts/__mocks__/@standardnotes/sncrypto-web.js b/packages/web/src/javascripts/__mocks__/@standardnotes/sncrypto-web.js new file mode 100644 index 000000000..9bd7885c4 --- /dev/null +++ b/packages/web/src/javascripts/__mocks__/@standardnotes/sncrypto-web.js @@ -0,0 +1,5 @@ +class SNWebCrypto { + initialize() {} +} + +module.exports = { SNWebCrypto }