diff --git a/packages/commonwealth/client/scripts/helpers/feature-flags.ts b/packages/commonwealth/client/scripts/helpers/feature-flags.ts index 2c61d3c8fe0..ed00bbcd8d7 100644 --- a/packages/commonwealth/client/scripts/helpers/feature-flags.ts +++ b/packages/commonwealth/client/scripts/helpers/feature-flags.ts @@ -27,6 +27,7 @@ const featureFlags = { process.env.FLAG_KNOCK_PUSH_NOTIFICATIONS_ENABLED, ), farcasterContest: buildFlag(process.env.FLAG_FARCASTER_CONTEST), + newEditor: buildFlag(process.env.FLAG_NEW_EDITOR), }; export type AvailableFeatureFlag = keyof typeof featureFlags; diff --git a/packages/commonwealth/client/scripts/navigation/CommonDomainRoutes.tsx b/packages/commonwealth/client/scripts/navigation/CommonDomainRoutes.tsx index 94741fb677b..4fac51647ad 100644 --- a/packages/commonwealth/client/scripts/navigation/CommonDomainRoutes.tsx +++ b/packages/commonwealth/client/scripts/navigation/CommonDomainRoutes.tsx @@ -4,7 +4,7 @@ import { Route } from 'react-router-dom'; import { withLayout } from 'views/Layout'; import { RouteFeatureFlags } from './Router'; -const EditorPage = lazy(() => import('views/pages/Editor')); +const EditorPage = lazy(() => import('views/pages/EditorPage')); const DashboardPage = lazy(() => import('views/pages/user_dashboard')); const CommunitiesPage = lazy(() => import('views/pages/Communities')); @@ -113,11 +113,7 @@ const CommonDomainRoutes = ({ contestEnabled, farcasterContestEnabled, }: RouteFeatureFlags) => [ - } - />, + } />, void; +}; + +export const Editor = memo(function Editor(props: EditorProps) { + const { onSubmit } = props; + const errorHandler = useEditorErrorHandler(); + const [dragging, setDragging] = useState(false); + const [uploading, setUploading] = useState(false); + + const dragCounterRef = useRef(0); + + const mode = props.mode ?? 'desktop'; + const imageHandler: ImageHandler = props.imageHandler ?? 'S3'; + + const placeholder = props.placeholder ?? 'Share your thoughts...'; + + const mdxEditorRef = React.useRef(null); + + const imageUploadHandlerDelegate = useImageUploadHandler(imageHandler); + + /** + * When we've stopped dragging, we also need to decrement the drag counter. + */ + const terminateDragging = useCallback(() => { + setDragging(false); + dragCounterRef.current = 0; + }, []); + + const imageUploadHandler = useCallback( + async (file: File) => { + try { + terminateDragging(); + setUploading(true); + return await imageUploadHandlerDelegate(file); + } finally { + setUploading(false); + } + }, + [imageUploadHandlerDelegate, terminateDragging], + ); + + const handleFile = useCallback(async (file: File) => { + if (!file.name.endsWith('.md')) { + notifyError('Not a markdown file.'); + return; + } + + const text = await fileToText(file); + + switch (DEFAULT_UPDATE_CONTENT_STRATEGY) { + case 'insert': + mdxEditorRef.current?.insertMarkdown(text); + break; + + case 'replace': + mdxEditorRef.current?.setMarkdown(text); + break; + } + }, []); + + const handleImportMarkdown = useCallback( + (file: File) => { + async function doAsync() { + await handleFile(file); + } + + doAsync().catch(console.error); + }, + [handleFile], + ); + + const handleFiles = useCallback( + async (files: FileList) => { + if (files.length === 1) { + const file = files[0]; + + if (canAcceptFileForImport(file)) { + await handleFile(file); + } else { + notifyError('File not markdown. Has invalid type: ' + file.type); + } + } + + if (files.length > 1) { + notifyError('Too many files given'); + return; + } + }, + [handleFile], + ); + + const handleDropAsync = useCallback( + async (event: React.DragEvent) => { + try { + const files = event.dataTransfer.files; + + await handleFiles(files); + } finally { + terminateDragging(); + } + }, + [handleFiles, terminateDragging], + ); + + const handleDrop = useCallback( + (event: React.DragEvent) => { + // ONLY handle this if it is markdown, else, allow the default paste + // handler to work + + const files = event.dataTransfer.files; + + if (files.length === 1) { + if (canAcceptFileForImport(files[0])) { + handleDropAsync(event).catch(console.error); + event.preventDefault(); + } + } + }, + [handleDropAsync], + ); + + const handleDragEnter = useCallback((event: React.DragEvent) => { + event.preventDefault(); + dragCounterRef.current = dragCounterRef.current + 1; + + if (dragCounterRef.current === 1) { + setDragging(true); + } + }, []); + + const handleDragOver = useCallback((event: React.DragEvent) => { + event.preventDefault(); + // This is necessary to allow a drop + event.dataTransfer!.dropEffect = 'copy'; // Shows a copy cursor when dragging files + }, []); + + const handleDragLeave = useCallback((event: React.DragEvent) => { + event.preventDefault(); + dragCounterRef.current = dragCounterRef.current - 1; + + if (dragCounterRef.current === 0) { + setDragging(false); + } + }, []); + + const handlePaste = useCallback( + (event: React.ClipboardEvent) => { + const files = event.clipboardData.files; + + if (files.length === 0) { + // now files here is acceptable because this could be a paste of other + // data like text/markdown, and we're relying on the MDXEditor paste + // handler to work + return; + } + + if (canAcceptFileForImport(files[0])) { + // if we can accept this file for import, go ahead and do so... + event.preventDefault(); + handleFiles(files).catch(console.error); + } + }, + [handleFiles], + ); + + const handleSubmit = useCallback(() => { + if (mdxEditorRef.current) { + const markdown = mdxEditorRef.current.getMarkdown(); + onSubmit?.(markdown); + } + }, [onSubmit]); + + return ( +
+
handlePaste(event)} + > + + mode === 'mobile' ? ( + + ) : ( + + ), + }), + listsPlugin(), + quotePlugin(), + headingsPlugin(), + linkPlugin(), + linkDialogPlugin(), + codeBlockPlugin({ defaultCodeBlockLanguage: 'js' }), + codeMirrorPlugin({ + codeBlockLanguages, + }), + imagePlugin({ imageUploadHandler }), + tablePlugin(), + thematicBreakPlugin(), + frontmatterPlugin(), + diffSourcePlugin({ viewMode: 'rich-text', diffMarkdown: 'boo' }), + markdownShortcutPlugin(), + ]} + /> + + {mode === 'desktop' && ( + + )} + + {dragging && } + {uploading && } +
+
+ ); +}); diff --git a/packages/commonwealth/client/scripts/views/components/Editor/README. md b/packages/commonwealth/client/scripts/views/components/Editor/README. md new file mode 100644 index 00000000000..57df7f53e58 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/Editor/README. md @@ -0,0 +1,61 @@ +# Overview + +Markdown Editor based on MDXEditor customized for our usage. + +It supports desktop and mobile mode. + +In desktop mode the UI is responsive and uses a static layout so it can +operate within the flow of a regular document. + +In mobile mode it docks the toolbar above the keyboard. + +# Fork + +This is currently using the commonwealth-mdxeditor, which is a fork of the +mdx-editor package. + +This is just temporary as I plan on merging this into the main MDXEditor when +we are done. + +Right now the only change is a 'location' property added to the toolbar code +so that we can place the toolbar below the editor. + +# Testing + +## Desktop + +- success: copy a .md file to the clipboard, try to paste it into the editor. It + should insert the content at the editor's cursor + + - this works via a File object (not text) so it's important to test this path. + +- success: drag a .md file on top of the editor. The drag indicator should show + up and cover the editor while you're dragging. Then the file should be inserted + at the cursor. + +- success: use the 'Import markdown' button to upload a file. + +- success: right click and copy an image in the browser, this should upload it +to the editor and insert it at the current point (I use msnbc.com for this as +their images are copyable and not CSS background images) + +- success: take a screenshot, try to paste it into the editor. The upload + indicator should show up. + +- success: use the image button at the top to manually upload an image. The + upload indicator should show up while this is happening. + +- success: drop an image file. Should upload it for us and not handle it as + markdown. + +- failure: copy multiple .md files ot the clipboard, try to paste into the editor. + It should fail because we can't handle multiple .md files + +## Mobile + +It's probably best to test this on a REAL mobile browser (not on a desktop). + +- The toolbar should be present at the bottom of the UI. + +- They keyboard should stay on top of the keyboard. + diff --git a/packages/commonwealth/client/scripts/views/pages/Editor/index.tsx b/packages/commonwealth/client/scripts/views/components/Editor/index.tsx similarity index 100% rename from packages/commonwealth/client/scripts/views/pages/Editor/index.tsx rename to packages/commonwealth/client/scripts/views/components/Editor/index.tsx diff --git a/packages/commonwealth/client/scripts/views/components/Editor/indicators/DragIndicator.tsx b/packages/commonwealth/client/scripts/views/components/Editor/indicators/DragIndicator.tsx new file mode 100644 index 00000000000..6267c56ac2a --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/Editor/indicators/DragIndicator.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +import { Indicator } from 'views/components/Editor/indicators/Indicator'; +import { CWIcon } from 'views/components/component_kit/cw_icons/cw_icon'; + +export const DragIndicator = () => { + return ( + + + + ); +}; diff --git a/packages/commonwealth/client/scripts/views/components/Editor/indicators/Indicator.scss b/packages/commonwealth/client/scripts/views/components/Editor/indicators/Indicator.scss new file mode 100644 index 00000000000..6cb8ec9ba0b --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/Editor/indicators/Indicator.scss @@ -0,0 +1,30 @@ +@import '../../../../../styles/shared'; + +.Indicator { + // cover the parent + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + // needed so that drag operations don't get hijacked by the indicator which + // would prevent dropping images and markdown files into the editor. + pointer-events: none; + + // the z-index is needed because the 'select' in MDXEditor has its own z-index + // which we have to sit on top of. + z-index: 10; + + // set the background color to grey-ish so that we can indicate something + // is 'muted' here. + background-color: rgba(128, 128, 128, 0.2); + + // needed so that the .inner progress indicator can be centered + display: flex; + + .inner { + // center the contnts + margin: auto auto; + } +} diff --git a/packages/commonwealth/client/scripts/views/components/Editor/indicators/Indicator.tsx b/packages/commonwealth/client/scripts/views/components/Editor/indicators/Indicator.tsx new file mode 100644 index 00000000000..6a4ef4f3f19 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/Editor/indicators/Indicator.tsx @@ -0,0 +1,16 @@ +import React, { ReactNode } from 'react'; + +import './Indicator.scss'; + +export type IndicatorProps = Readonly<{ + children: ReactNode; +}>; + +export const Indicator = (props: IndicatorProps) => { + const { children } = props; + return ( +
+
{children}
+
+ ); +}; diff --git a/packages/commonwealth/client/scripts/views/components/Editor/indicators/UploadIndicator.tsx b/packages/commonwealth/client/scripts/views/components/Editor/indicators/UploadIndicator.tsx new file mode 100644 index 00000000000..af8d2fe42f5 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/Editor/indicators/UploadIndicator.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +import CWCircleRingSpinner from 'views/components/component_kit/new_designs/CWCircleRingSpinner'; +import { Indicator } from 'views/components/Editor/indicators/Indicator'; + +export const UploadIndicator = () => { + return ( + + + + ); +}; diff --git a/packages/commonwealth/client/scripts/views/pages/Editor/markdown/gfm.md b/packages/commonwealth/client/scripts/views/components/Editor/markdown/gfm.md similarity index 100% rename from packages/commonwealth/client/scripts/views/pages/Editor/markdown/gfm.md rename to packages/commonwealth/client/scripts/views/components/Editor/markdown/gfm.md diff --git a/packages/commonwealth/client/scripts/views/pages/Editor/markdown/markdown.md b/packages/commonwealth/client/scripts/views/components/Editor/markdown/markdown.md similarity index 100% rename from packages/commonwealth/client/scripts/views/pages/Editor/markdown/markdown.md rename to packages/commonwealth/client/scripts/views/components/Editor/markdown/markdown.md diff --git a/packages/commonwealth/client/scripts/views/pages/Editor/markdown/supported.md b/packages/commonwealth/client/scripts/views/components/Editor/markdown/supported.md similarity index 83% rename from packages/commonwealth/client/scripts/views/pages/Editor/markdown/supported.md rename to packages/commonwealth/client/scripts/views/components/Editor/markdown/supported.md index 61f35ced46b..89772fe41fc 100644 --- a/packages/commonwealth/client/scripts/views/pages/Editor/markdown/supported.md +++ b/packages/commonwealth/client/scripts/views/components/Editor/markdown/supported.md @@ -1,5 +1,17 @@ # Example of all supported markdown features +This is a basic demo of the markdown editor. + +You can append to the URL ```?mode=desktop``` or ```?mode=mobile``` to test out a specific mode. + +The mobile version works in a desktop browser but should really only be run inside a real mobile browser when verifying functionality. + +When you hit submit, it will print the markdown to the console. + +Image uploads are local and do not go to S3 to avoid polluting our S3 bucket. + +# Markdown Rendering Tests + This just tests all the major markdown features we want to support. # Header 1 diff --git a/packages/commonwealth/client/scripts/views/components/Editor/toolbars/ToolbarForDesktop.scss b/packages/commonwealth/client/scripts/views/components/Editor/toolbars/ToolbarForDesktop.scss new file mode 100644 index 00000000000..5b33f4e618b --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/Editor/toolbars/ToolbarForDesktop.scss @@ -0,0 +1,5 @@ +.ToolbarForDesktop { + display: flex; + flex-grow: 1; + user-select: none; +} diff --git a/packages/commonwealth/client/scripts/views/components/Editor/toolbars/ToolbarForDesktop.tsx b/packages/commonwealth/client/scripts/views/components/Editor/toolbars/ToolbarForDesktop.tsx new file mode 100644 index 00000000000..bb19185ce35 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/Editor/toolbars/ToolbarForDesktop.tsx @@ -0,0 +1,50 @@ +import { + BlockTypeSelect, + BoldItalicUnderlineToggles, + ChangeCodeMirrorLanguage, + ConditionalContents, + CreateLink, + InsertCodeBlock, + InsertImage, + InsertTable, + ListsToggle, + Separator, + StrikeThroughSupSubToggles, +} from 'commonwealth-mdxeditor'; +import React from 'react'; + +import './ToolbarForDesktop.scss'; + +export const ToolbarForDesktop = () => { + return ( +
+ editor?.editorType === 'codeblock', + contents: () => , + }, + { + fallback: () => ( + <> +
+ +
+ + + + + + + + + + + + ), + }, + ]} + /> +
+ ); +}; diff --git a/packages/commonwealth/client/scripts/views/pages/Editor/toolbars/ToolbarForMobile.scss b/packages/commonwealth/client/scripts/views/components/Editor/toolbars/ToolbarForMobile.scss similarity index 52% rename from packages/commonwealth/client/scripts/views/pages/Editor/toolbars/ToolbarForMobile.scss rename to packages/commonwealth/client/scripts/views/components/Editor/toolbars/ToolbarForMobile.scss index d734db701d9..03f79736f92 100644 --- a/packages/commonwealth/client/scripts/views/pages/Editor/toolbars/ToolbarForMobile.scss +++ b/packages/commonwealth/client/scripts/views/components/Editor/toolbars/ToolbarForMobile.scss @@ -1,7 +1,13 @@ .ToolbarForMobile { + display: flex; + flex-grow: 1; .end { justify-content: flex-end; flex-grow: 1; display: flex; } + + .mdxeditor-toolbar { + height: inherit !important; + } } diff --git a/packages/commonwealth/client/scripts/views/pages/Editor/toolbars/ToolbarForMobile.tsx b/packages/commonwealth/client/scripts/views/components/Editor/toolbars/ToolbarForMobile.tsx similarity index 58% rename from packages/commonwealth/client/scripts/views/pages/Editor/toolbars/ToolbarForMobile.tsx rename to packages/commonwealth/client/scripts/views/components/Editor/toolbars/ToolbarForMobile.tsx index 84889e00c21..bed706f90f4 100644 --- a/packages/commonwealth/client/scripts/views/pages/Editor/toolbars/ToolbarForMobile.tsx +++ b/packages/commonwealth/client/scripts/views/components/Editor/toolbars/ToolbarForMobile.tsx @@ -10,10 +10,16 @@ import React from 'react'; import './ToolbarForMobile.scss'; -export const ToolbarForMobile = () => { +type ToolbarForMobileProps = Readonly<{ + onSubmit?: () => void; +}>; + +export const ToolbarForMobile = (props: ToolbarForMobileProps) => { + const { onSubmit } = props; + return ( - <> -
+
+
{/**/} @@ -23,8 +29,8 @@ export const ToolbarForMobile = () => {
- +
- +
); }; diff --git a/packages/commonwealth/client/scripts/views/pages/Editor/useEditorErrorHandler.ts b/packages/commonwealth/client/scripts/views/components/Editor/useEditorErrorHandler.ts similarity index 100% rename from packages/commonwealth/client/scripts/views/pages/Editor/useEditorErrorHandler.ts rename to packages/commonwealth/client/scripts/views/components/Editor/useEditorErrorHandler.ts diff --git a/packages/commonwealth/client/scripts/views/components/Editor/useImageUploadHandler.ts b/packages/commonwealth/client/scripts/views/components/Editor/useImageUploadHandler.ts new file mode 100644 index 00000000000..ecb1053ff58 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/Editor/useImageUploadHandler.ts @@ -0,0 +1,41 @@ +import { notifyError } from 'controllers/app/notifications'; +import { useCallback } from 'react'; +import { ImageHandler, ImageURL } from './Editor'; +import { useImageUploadHandlerLocal } from './useImageUploadHandlerLocal'; +import { useImageUploadHandlerS3 } from './useImageUploadHandlerS3'; +import { useImageUploadHandlerWithFailure } from './useImageUploadHandlerWithFailure'; + +/** + * Handles supporting either of our image handlers. + */ +export function useImageUploadHandler(imageHandler: ImageHandler) { + const imageUploadHandlerDelegateLocal = useImageUploadHandlerLocal(); + const imageUploadHandlerDelegateS3 = useImageUploadHandlerS3(); + const imageUploadHandlerDelegateWithFailure = + useImageUploadHandlerWithFailure(); + + return useCallback( + async (file: File): Promise => { + try { + switch (imageHandler) { + case 'S3': + return await imageUploadHandlerDelegateS3(file); + case 'local': + return await imageUploadHandlerDelegateLocal(file); + case 'failure': + return await imageUploadHandlerDelegateWithFailure(); + } + } catch (e) { + notifyError('Failed to upload image: ' + e.message); + } + + throw new Error('Unknown image handler: ' + imageHandler); + }, + [ + imageHandler, + imageUploadHandlerDelegateLocal, + imageUploadHandlerDelegateS3, + imageUploadHandlerDelegateWithFailure, + ], + ); +} diff --git a/packages/commonwealth/client/scripts/views/pages/Editor/useImageUploadHandlerLocal.ts b/packages/commonwealth/client/scripts/views/components/Editor/useImageUploadHandlerLocal.ts similarity index 100% rename from packages/commonwealth/client/scripts/views/pages/Editor/useImageUploadHandlerLocal.ts rename to packages/commonwealth/client/scripts/views/components/Editor/useImageUploadHandlerLocal.ts diff --git a/packages/commonwealth/client/scripts/views/pages/Editor/useImageUploadHandlerS3.ts b/packages/commonwealth/client/scripts/views/components/Editor/useImageUploadHandlerS3.ts similarity index 91% rename from packages/commonwealth/client/scripts/views/pages/Editor/useImageUploadHandlerS3.ts rename to packages/commonwealth/client/scripts/views/components/Editor/useImageUploadHandlerS3.ts index 51664972177..74444307363 100644 --- a/packages/commonwealth/client/scripts/views/pages/Editor/useImageUploadHandlerS3.ts +++ b/packages/commonwealth/client/scripts/views/components/Editor/useImageUploadHandlerS3.ts @@ -2,7 +2,7 @@ import { useCallback } from 'react'; import { SERVER_URL } from 'state/api/config'; import useUserStore from 'state/ui/user'; import { uploadFileToS3 } from 'views/components/react_quill_editor/utils'; -import { ImageURL } from 'views/pages/Editor/Editor'; +import { ImageURL } from './Editor'; /** * This is the main/default image handler for S3. diff --git a/packages/commonwealth/client/scripts/views/components/Editor/useImageUploadHandlerWithFailure.ts b/packages/commonwealth/client/scripts/views/components/Editor/useImageUploadHandlerWithFailure.ts new file mode 100644 index 00000000000..8c60d081d89 --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/Editor/useImageUploadHandlerWithFailure.ts @@ -0,0 +1,12 @@ +import { delay } from '@hicommonwealth/shared'; +import { useCallback } from 'react'; + +/** + * Fake image upload handler that just fails + */ +export function useImageUploadHandlerWithFailure() { + return useCallback(async () => { + await delay(1000); + throw new Error('Image upload failed successfully.'); + }, []); +} diff --git a/packages/commonwealth/client/scripts/views/components/Editor/utils/canAcceptFileForImport.ts b/packages/commonwealth/client/scripts/views/components/Editor/utils/canAcceptFileForImport.ts new file mode 100644 index 00000000000..72156348cef --- /dev/null +++ b/packages/commonwealth/client/scripts/views/components/Editor/utils/canAcceptFileForImport.ts @@ -0,0 +1,3 @@ +export function canAcceptFileForImport(file: Pick) { + return ['text/markdown', 'text/plain'].includes(file.type); +} diff --git a/packages/commonwealth/client/scripts/views/pages/Editor/utils/codeBlockLanguages.ts b/packages/commonwealth/client/scripts/views/components/Editor/utils/codeBlockLanguages.ts similarity index 100% rename from packages/commonwealth/client/scripts/views/pages/Editor/utils/codeBlockLanguages.ts rename to packages/commonwealth/client/scripts/views/components/Editor/utils/codeBlockLanguages.ts diff --git a/packages/commonwealth/client/scripts/views/pages/Editor/utils/editorTranslator.ts b/packages/commonwealth/client/scripts/views/components/Editor/utils/editorTranslator.ts similarity index 100% rename from packages/commonwealth/client/scripts/views/pages/Editor/utils/editorTranslator.ts rename to packages/commonwealth/client/scripts/views/components/Editor/utils/editorTranslator.ts diff --git a/packages/commonwealth/client/scripts/views/pages/Editor/utils/fileToText.ts b/packages/commonwealth/client/scripts/views/components/Editor/utils/fileToText.ts similarity index 100% rename from packages/commonwealth/client/scripts/views/pages/Editor/utils/fileToText.ts rename to packages/commonwealth/client/scripts/views/components/Editor/utils/fileToText.ts diff --git a/packages/commonwealth/client/scripts/views/pages/Editor/utils/iconComponentFor.tsx b/packages/commonwealth/client/scripts/views/components/Editor/utils/iconComponentFor.tsx similarity index 95% rename from packages/commonwealth/client/scripts/views/pages/Editor/utils/iconComponentFor.tsx rename to packages/commonwealth/client/scripts/views/components/Editor/utils/iconComponentFor.tsx index 9e4369d4cd9..fae7d43b895 100644 --- a/packages/commonwealth/client/scripts/views/pages/Editor/utils/iconComponentFor.tsx +++ b/packages/commonwealth/client/scripts/views/components/Editor/utils/iconComponentFor.tsx @@ -1,6 +1,6 @@ import { defaultSvgIcons, IconKey } from 'commonwealth-mdxeditor'; import React from 'react'; -import { CWIcon } from 'views/components/component_kit/cw_icons/cw_icon'; +import { CWIcon } from '../../component_kit/cw_icons/cw_icon'; const DEFAULT_ICON_SIZE = 'regular'; diff --git a/packages/commonwealth/client/scripts/views/components/component_kit/cw_icons/cw_icon_lookup.ts b/packages/commonwealth/client/scripts/views/components/component_kit/cw_icons/cw_icon_lookup.ts index 2565f34f950..31a63a00949 100644 --- a/packages/commonwealth/client/scripts/views/components/component_kit/cw_icons/cw_icon_lookup.ts +++ b/packages/commonwealth/client/scripts/views/components/component_kit/cw_icons/cw_icon_lookup.ts @@ -25,6 +25,7 @@ import { CircleNotch, CirclesThreePlus, ClockCounterClockwise, + CloudArrowUp, Code, Coins, Compass, @@ -58,12 +59,16 @@ import { PlusCircle, PushPin, Question, + Quotes, Rows, SignOut, Sparkle, SquaresFour, Table, TextB, + TextHOne, + TextHThree, + TextHTwo, TextItalic, TextStrikethrough, TextSubscript, @@ -91,7 +96,12 @@ export const iconLookup = { listDashes: withPhosphorIcon(ListDashes), listNumbers: withPhosphorIcon(ListNumbers), listChecks: withPhosphorIcon(ListChecks), + h1: withPhosphorIcon(TextHOne), + h2: withPhosphorIcon(TextHTwo), + h3: withPhosphorIcon(TextHThree), + quotes: withPhosphorIcon(Quotes), table: withPhosphorIcon(Table), + cloudArrowUp: withPhosphorIcon(CloudArrowUp), archiveTrayFilled: Icons.CWArchiveTrayFilled, arrowDownBlue500: Icons.CWArrowDownBlue500, arrowFatUp: Icons.CWArrowFatUp, diff --git a/packages/commonwealth/client/scripts/views/pages/Editor/DragIndicator.scss b/packages/commonwealth/client/scripts/views/pages/Editor/DragIndicator.scss deleted file mode 100644 index 3d2071da501..00000000000 --- a/packages/commonwealth/client/scripts/views/pages/Editor/DragIndicator.scss +++ /dev/null @@ -1,11 +0,0 @@ -@import '../../../../styles/shared'; - -.DragIndicator { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 10; - background-color: $neutral-600; -} diff --git a/packages/commonwealth/client/scripts/views/pages/Editor/DragIndicator.tsx b/packages/commonwealth/client/scripts/views/pages/Editor/DragIndicator.tsx deleted file mode 100644 index e52511d1e22..00000000000 --- a/packages/commonwealth/client/scripts/views/pages/Editor/DragIndicator.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -import './DragIndicator.scss'; - -export const DragIndicator = () => { - return
; -}; diff --git a/packages/commonwealth/client/scripts/views/pages/Editor/Editor.tsx b/packages/commonwealth/client/scripts/views/pages/Editor/Editor.tsx deleted file mode 100644 index b8d70c1eac5..00000000000 --- a/packages/commonwealth/client/scripts/views/pages/Editor/Editor.tsx +++ /dev/null @@ -1,216 +0,0 @@ -import { - codeBlockPlugin, - codeMirrorPlugin, - diffSourcePlugin, - frontmatterPlugin, - headingsPlugin, - imagePlugin, - linkDialogPlugin, - linkPlugin, - listsPlugin, - markdownShortcutPlugin, - MDXEditor, - MDXEditorMethods, - quotePlugin, - tablePlugin, - thematicBreakPlugin, - toolbarPlugin, -} from 'commonwealth-mdxeditor'; -import React, { memo, useCallback, useRef, useState } from 'react'; - -import './Editor.scss'; - -import clsx from 'clsx'; -import 'commonwealth-mdxeditor/style.css'; -import { notifyError } from 'controllers/app/notifications'; -import { DesktopEditorFooter } from 'views/pages/Editor/DesktopEditorFooter'; -import { DragIndicator } from 'views/pages/Editor/DragIndicator'; -import { ToolbarForDesktop } from 'views/pages/Editor/toolbars/ToolbarForDesktop'; -import { ToolbarForMobile } from 'views/pages/Editor/toolbars/ToolbarForMobile'; -import { useEditorErrorHandler } from 'views/pages/Editor/useEditorErrorHandler'; -import { useImageUploadHandler } from 'views/pages/Editor/useImageUploadHandler'; -import { codeBlockLanguages } from 'views/pages/Editor/utils/codeBlockLanguages'; -import { editorTranslator } from 'views/pages/Editor/utils/editorTranslator'; -import { fileToText } from 'views/pages/Editor/utils/fileToText'; -import { iconComponentFor } from 'views/pages/Editor/utils/iconComponentFor'; -import supported from './markdown/supported.md?raw'; - -export type ImageURL = string; - -export type EditorMode = 'desktop' | 'mobile'; - -export type ImageHandler = 'S3' | 'local'; - -type EditorProps = { - readonly mode?: EditorMode; - readonly placeholder?: string; - readonly imageHandler?: ImageHandler; -}; - -export const Editor = memo(function Editor(props: EditorProps) { - const errorHandler = useEditorErrorHandler(); - const [dragging, setDragging] = useState(false); - const [uploading, setUploading] = useState(false); - - const dragCounterRef = useRef(0); - - const mode = props.mode ?? 'desktop'; - const imageHandler: ImageHandler = props.imageHandler ?? 'S3'; - - const placeholder = props.placeholder ?? 'Share your thoughts...'; - - const mdxEditorRef = React.useRef(null); - - const imageUploadHandlerDelegate = useImageUploadHandler(imageHandler); - - const imageUploadHandler = useCallback( - async (file: File) => { - try { - // TODO: - setUploading(true); - return await imageUploadHandlerDelegate(file); - } finally { - setUploading(false); - } - }, - [imageUploadHandlerDelegate], - ); - - const handleFile = useCallback(async (file: File) => { - if (!file.name.endsWith('.md')) { - notifyError('Not a markdown file.'); - return; - } - - const text = await fileToText(file); - mdxEditorRef.current?.setMarkdown(text); - }, []); - - const handleImportMarkdown = useCallback( - (file: File) => { - async function doAsync() { - await handleFile(file); - } - - doAsync().catch(console.error); - }, - [handleFile], - ); - - const handleDropAsync = useCallback( - async (event: React.DragEvent) => { - const nrFiles = event.dataTransfer.files.length; - - try { - if (nrFiles === 1) { - const type = event.dataTransfer.files[0].type; - - if (['text/markdown', 'text/plain'].includes(type)) { - await handleFile(event.dataTransfer.files[0]); - } else { - notifyError('File not markdown. Has invalid type: ' + type); - } - } - - if (nrFiles <= 0) { - notifyError('No files given'); - return; - } - - if (nrFiles > 1) { - notifyError('Too many files given'); - return; - } - } finally { - setDragging(false); - } - }, - [handleFile], - ); - - // TODO: handle html files but I'm not sure about the correct way to handle it - // because I have to convert to markdown. This isn't really a typical use - // case though - const handleDrop = useCallback( - (event: React.DragEvent) => { - event.preventDefault(); - handleDropAsync(event).catch(console.error); - }, - [handleDropAsync], - ); - - const handleDragEnter = useCallback((event: React.DragEvent) => { - event.preventDefault(); - dragCounterRef.current = dragCounterRef.current + 1; - - if (dragCounterRef.current === 1) { - setDragging(true); - } - }, []); - - const handleDragOver = useCallback((event: React.DragEvent) => { - event.preventDefault(); - // This is necessary to allow a drop - event.dataTransfer!.dropEffect = 'copy'; // Shows a copy cursor when dragging files - }, []); - - const handleDragLeave = useCallback((event: React.DragEvent) => { - event.preventDefault(); - dragCounterRef.current = dragCounterRef.current - 1; - - if (dragCounterRef.current === 0) { - setDragging(false); - } - }, []); - - return ( -
- - mode === 'mobile' ? : , - }), - listsPlugin(), - quotePlugin(), - headingsPlugin(), - linkPlugin(), - linkDialogPlugin(), - codeBlockPlugin({ defaultCodeBlockLanguage: 'js' }), - codeMirrorPlugin({ - codeBlockLanguages, - }), - imagePlugin({ imageUploadHandler }), - tablePlugin(), - thematicBreakPlugin(), - frontmatterPlugin(), - diffSourcePlugin({ viewMode: 'rich-text', diffMarkdown: 'boo' }), - markdownShortcutPlugin(), - ]} - /> - - {mode === 'desktop' && ( - - )} - - {dragging && } - {uploading && } -
- ); -}); diff --git a/packages/commonwealth/client/scripts/views/pages/Editor/README. md b/packages/commonwealth/client/scripts/views/pages/Editor/README. md deleted file mode 100644 index ca438343107..00000000000 --- a/packages/commonwealth/client/scripts/views/pages/Editor/README. md +++ /dev/null @@ -1,7 +0,0 @@ -This is currently using the commonwealth-mdxeditor which is a fork of the -mdeditor package. - -This is just temporary as I plan on merging this into the main MDXEditor when -we are done. - -Then we can switch over to the real one. diff --git a/packages/commonwealth/client/scripts/views/pages/Editor/toolbars/ToolbarForDesktop.tsx b/packages/commonwealth/client/scripts/views/pages/Editor/toolbars/ToolbarForDesktop.tsx deleted file mode 100644 index aa050c0b208..00000000000 --- a/packages/commonwealth/client/scripts/views/pages/Editor/toolbars/ToolbarForDesktop.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { - BlockTypeSelect, - BoldItalicUnderlineToggles, - CreateLink, - InsertCodeBlock, - InsertImage, - InsertTable, - ListsToggle, - Separator, - StrikeThroughSupSubToggles, -} from 'commonwealth-mdxeditor'; -import React from 'react'; - -export const ToolbarForDesktop = () => { - return ( - <> -
- -
- {/**/} - - - - - - - - - - - - ); -}; diff --git a/packages/commonwealth/client/scripts/views/pages/Editor/useImageUploadHandler.ts b/packages/commonwealth/client/scripts/views/pages/Editor/useImageUploadHandler.ts deleted file mode 100644 index 8b781ec9a4b..00000000000 --- a/packages/commonwealth/client/scripts/views/pages/Editor/useImageUploadHandler.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useCallback } from 'react'; -import { ImageHandler, ImageURL } from 'views/pages/Editor/Editor'; -import { useImageUploadHandlerLocal } from 'views/pages/Editor/useImageUploadHandlerLocal'; -import { useImageUploadHandlerS3 } from 'views/pages/Editor/useImageUploadHandlerS3'; - -/** - * Handles supporting either of our image handlers. - */ -export function useImageUploadHandler(imageHandler: ImageHandler) { - const imageUploadHandlerDelegateLocal = useImageUploadHandlerLocal(); - const imageUploadHandlerDelegateS3 = useImageUploadHandlerS3(); - - return useCallback( - async (file: File): Promise => { - switch (imageHandler) { - case 'S3': - return await imageUploadHandlerDelegateS3(file); - case 'local': - return await imageUploadHandlerDelegateLocal(file); - } - - throw new Error('Unknown image handler: ' + imageHandler); - }, - [ - imageHandler, - imageUploadHandlerDelegateLocal, - imageUploadHandlerDelegateS3, - ], - ); -} diff --git a/packages/commonwealth/client/scripts/views/pages/EditorPage/EditorPage.tsx b/packages/commonwealth/client/scripts/views/pages/EditorPage/EditorPage.tsx new file mode 100644 index 00000000000..52a97ba21db --- /dev/null +++ b/packages/commonwealth/client/scripts/views/pages/EditorPage/EditorPage.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { useSearchParams } from 'react-router-dom'; +import Editor from 'views/components/Editor'; +import { EditorMode } from 'views/components/Editor/Editor'; + +import supported from 'views/components/Editor/markdown/supported.md?raw'; + +function useParams() { + const [searchParams] = useSearchParams(); + const mode = searchParams.get('mode') ?? 'desktop'; + return { + mode: mode as EditorMode, + }; +} + +/** + * Basic demo page that allows us to use either mode and to log the markdown. + */ +export const EditorPage = () => { + const { mode } = useParams(); + + return ( + console.log('markdown: \n' + markdown)} + /> + ); +}; diff --git a/packages/commonwealth/client/scripts/views/pages/EditorPage/index.tsx b/packages/commonwealth/client/scripts/views/pages/EditorPage/index.tsx new file mode 100644 index 00000000000..60e184225bd --- /dev/null +++ b/packages/commonwealth/client/scripts/views/pages/EditorPage/index.tsx @@ -0,0 +1,3 @@ +import { EditorPage } from 'views/pages/EditorPage/EditorPage'; + +export default EditorPage; diff --git a/packages/commonwealth/client/vite.config.ts b/packages/commonwealth/client/vite.config.ts index ec66df0cb12..772e09ba80e 100644 --- a/packages/commonwealth/client/vite.config.ts +++ b/packages/commonwealth/client/vite.config.ts @@ -32,6 +32,7 @@ export default defineConfig(({ mode }) => { // WARN: only used locally never in remote (Heroku) apps const featureFlags = { + 'process.env.FLAG_NEW_EDITOR': JSON.stringify(env.FLAG_NEW_EDITOR), 'process.env.FLAG_CONTEST': JSON.stringify(env.FLAG_CONTEST), 'process.env.FLAG_CONTEST_DEV': JSON.stringify(env.FLAG_CONTEST_DEV), 'process.env.FLAG_KNOCK_PUSH_NOTIFICATIONS_ENABLED': JSON.stringify(