From cc2762a29d83e2e4cd646345f9d3935571b27c62 Mon Sep 17 00:00:00 2001 From: Mo Date: Mon, 28 Nov 2022 12:19:53 -0600 Subject: [PATCH] feat: convert Super notes to Markdown behind the scenes while publishing to Listed (#2064) --- .github/workflows/desktop.release.reuse.yml | 3 +- packages/snjs/lib/Application/Application.ts | 4 +++ .../lib/Services/Actions/ActionsService.ts | 36 ++++++++++++++++++- .../GetMarkdownPlugin/GetMarkdownPlugin.tsx | 28 +++++++++++++++ .../NoteView/SuperEditor/SuperEditor.tsx | 18 ++++++++++ 5 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/GetMarkdownPlugin/GetMarkdownPlugin.tsx diff --git a/.github/workflows/desktop.release.reuse.yml b/.github/workflows/desktop.release.reuse.yml index 87fc1af54..0cc1bcfd5 100644 --- a/.github/workflows/desktop.release.reuse.yml +++ b/.github/workflows/desktop.release.reuse.yml @@ -160,7 +160,6 @@ jobs: run: working-directory: packages/desktop steps: - - run: echo APP_VERSION=$(node -p "require('./../web/package.json').version") >> $GITHUB_ENV - uses: actions/checkout@v3 - uses: actions/setup-node@v3 @@ -169,6 +168,8 @@ jobs: node-version-file: '.nvmrc' cache: 'yarn' + - run: echo APP_VERSION=$(node -p "require('./../web/package.json').version") >> $GITHUB_ENV + - run: yarn install --immutable - uses: actions/download-artifact@v3 diff --git a/packages/snjs/lib/Application/Application.ts b/packages/snjs/lib/Application/Application.ts index 14cbccc71..37a9ef607 100644 --- a/packages/snjs/lib/Application/Application.ts +++ b/packages/snjs/lib/Application/Application.ts @@ -295,6 +295,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return this.diskStorageService } + public get actions(): InternalServices.SNActionsService { + return this.actionsManager + } + public computePrivateUsername(username: string): Promise { return ComputePrivateUsername(this.options.crypto, username) } diff --git a/packages/snjs/lib/Services/Actions/ActionsService.ts b/packages/snjs/lib/Services/Actions/ActionsService.ts index e7ac2ca61..745f894da 100644 --- a/packages/snjs/lib/Services/Actions/ActionsService.ts +++ b/packages/snjs/lib/Services/Actions/ActionsService.ts @@ -1,3 +1,4 @@ +import { removeFromArray } from '@standardnotes/utils' import { SNRootKey } from '@standardnotes/encryption' import { ChallengeService } from '../Challenge' import { ListedService } from '../Listed/ListedService' @@ -18,6 +19,8 @@ import { isErrorDecryptingPayload, CreateEncryptedBackupFileContextPayload, EncryptedTransferPayload, + TransferPayload, + ItemContent, } from '@standardnotes/models' import { SNSyncService } from '../Sync/SyncService' import { PayloadManager } from '../Payloads/PayloadManager' @@ -34,6 +37,8 @@ import { Challenge, } from '@standardnotes/services' +type PayloadRequestHandler = (uuid: string) => TransferPayload | undefined + /** * The Actions Service allows clients to interact with action-based extensions. * Action-based extensions are mostly RESTful actions that can push a local value or @@ -50,6 +55,7 @@ import { */ export class SNActionsService extends AbstractService { private previousPasswords: string[] = [] + private payloadRequestHandlers: PayloadRequestHandler[] = [] constructor( private itemManager: ItemManager, @@ -81,6 +87,14 @@ export class SNActionsService extends AbstractService { super.deinit() } + public addPayloadRequestHandler(handler: PayloadRequestHandler) { + this.payloadRequestHandlers.push(handler) + + return () => { + removeFromArray(this.payloadRequestHandlers, handler) + } + } + public getExtensions(): SNActionsExtension[] { const extensionItems = this.itemManager.getItems(ContentType.ActionsExtension) const excludingListed = extensionItems.filter((extension) => !extension.isListedExtension) @@ -312,7 +326,16 @@ export class SNActionsService extends AbstractService { return {} as ActionResponse } - private async outgoingPayloadForItem(item: DecryptedItemInterface, decrypted = false) { + private async outgoingPayloadForItem( + item: DecryptedItemInterface, + decrypted = false, + ): Promise> { + const payloadFromHandler = this.getPayloadFromRequestHandlers(item.uuid) + + if (payloadFromHandler) { + return payloadFromHandler + } + if (decrypted) { return item.payload.ejected() } @@ -323,4 +346,15 @@ export class SNActionsService extends AbstractService { return CreateEncryptedBackupFileContextPayload(encrypted) } + + private getPayloadFromRequestHandlers(uuid: string): TransferPayload | undefined { + for (const handler of this.payloadRequestHandlers) { + const payload = handler(uuid) + if (payload) { + return payload + } + } + + return undefined + } } diff --git a/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/GetMarkdownPlugin/GetMarkdownPlugin.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/GetMarkdownPlugin/GetMarkdownPlugin.tsx new file mode 100644 index 000000000..afffe5ee7 --- /dev/null +++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/Plugins/GetMarkdownPlugin/GetMarkdownPlugin.tsx @@ -0,0 +1,28 @@ +import { forwardRef, useCallback, useImperativeHandle } from 'react' +import { $convertToMarkdownString } from '@lexical/markdown' +import { MarkdownTransformers } from '@standardnotes/blocks-editor' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' + +export type GetMarkdownPluginInterface = { + getMarkdown: () => string +} + +const GetMarkdownPlugin = forwardRef((_, ref) => { + const [editor] = useLexicalComposerContext() + + useImperativeHandle(ref, () => ({ + getMarkdown() { + return getMarkdown() + }, + })) + + const getMarkdown = useCallback(() => { + return editor.getEditorState().read(() => { + return $convertToMarkdownString(MarkdownTransformers) + }) + }, [editor]) + + return null +}) + +export default GetMarkdownPlugin diff --git a/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperEditor.tsx b/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperEditor.tsx index b76bf6314..3f3f4c950 100644 --- a/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperEditor.tsx +++ b/packages/web/src/javascripts/Components/NoteView/SuperEditor/SuperEditor.tsx @@ -27,6 +27,7 @@ import { useCommandService } from '@/Components/ApplicationView/CommandProvider' import { SUPER_SHOW_MARKDOWN_PREVIEW } from '@standardnotes/ui-services' import { SuperNoteMarkdownPreview } from './SuperNoteMarkdownPreview' import { ExportPlugin } from './Plugins/ExportPlugin/ExportPlugin' +import GetMarkdownPlugin, { GetMarkdownPluginInterface } from './Plugins/GetMarkdownPlugin/GetMarkdownPlugin' import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin' const NotePreviewCharLimit = 160 @@ -50,6 +51,7 @@ export const SuperEditor: FunctionComponent = ({ const changeEditorFunction = useRef() const ignoreNextChange = useRef(false) const [showMarkdownPreview, setShowMarkdownPreview] = useState(false) + const getMarkdownPlugin = useRef(null) const commandService = useCommandService() @@ -64,6 +66,21 @@ export const SuperEditor: FunctionComponent = ({ setShowMarkdownPreview(false) }, []) + useEffect(() => { + return application.actions.addPayloadRequestHandler((uuid) => { + if (uuid === note.current.uuid) { + const basePayload = note.current.payload.ejected() + return { + ...basePayload, + content: { + ...basePayload.content, + text: getMarkdownPlugin.current?.getMarkdown() ?? basePayload.content.text, + }, + } + } + }) + }, [application]) + const [lineHeight, setLineHeight] = useState(PrefDefaults[PrefKey.EditorLineHeight]) const handleChange = useCallback( @@ -149,6 +166,7 @@ export const SuperEditor: FunctionComponent = ({ +