diff --git a/libs/model/src/comment/CreateComment.command.ts b/libs/model/src/comment/CreateComment.command.ts index 65a9ce21732..c0d88853af6 100644 --- a/libs/model/src/comment/CreateComment.command.ts +++ b/libs/model/src/comment/CreateComment.command.ts @@ -3,7 +3,7 @@ import * as schemas from '@hicommonwealth/schemas'; import { models } from '../database'; import { isAuthorized, type AuthContext } from '../middleware'; import { verifyCommentSignature } from '../middleware/canvas'; -import { mustExist } from '../middleware/guards'; +import { mustBeAuthorizedThread, mustExist } from '../middleware/guards'; import { emitEvent, emitMentions, @@ -32,25 +32,15 @@ export function CreateComment(): Command< isAuthorized({ action: schemas.PermissionEnum.CREATE_COMMENT }), verifyCommentSignature, ], - body: async ({ actor, payload }) => { - const { thread_id, parent_id, ...rest } = payload; + body: async ({ actor, payload, auth }) => { + const { address, thread } = mustBeAuthorizedThread(actor, auth); - const thread = await models.Thread.findOne({ where: { id: thread_id } }); - mustExist('Thread', thread); if (thread.read_only) throw new InvalidState(CreateCommentErrors.CantCommentOnReadOnly); if (thread.archived_at) throw new InvalidState(CreateCommentErrors.ThreadArchived); - const address = await models.Address.findOne({ - where: { - community_id: thread.community_id, - user_id: actor.user.id, - address: actor.address, - }, - }); - mustExist('Community address', address); - + const { thread_id, parent_id, ...rest } = payload; if (parent_id) { const parent = await models.Comment.findOne({ where: { id: parent_id, thread_id }, diff --git a/libs/model/src/comment/CreateCommentReaction.command.ts b/libs/model/src/comment/CreateCommentReaction.command.ts index 8f1f93dbc5e..373d433f611 100644 --- a/libs/model/src/comment/CreateCommentReaction.command.ts +++ b/libs/model/src/comment/CreateCommentReaction.command.ts @@ -3,7 +3,7 @@ import * as schemas from '@hicommonwealth/schemas'; import { models } from '../database'; import { isAuthorized, type AuthContext } from '../middleware'; import { verifyReactionSignature } from '../middleware/canvas'; -import { mustExist } from '../middleware/guards'; +import { mustBeAuthorizedComment } from '../middleware/guards'; import { getVotingWeight } from '../services/stakeHelper'; export function CreateCommentReaction(): Command< @@ -16,27 +16,9 @@ export function CreateCommentReaction(): Command< isAuthorized({ action: schemas.PermissionEnum.CREATE_COMMENT_REACTION }), verifyReactionSignature, ], - body: async ({ actor, payload }) => { - const comment = await models.Comment.findOne({ - where: { id: payload.comment_id }, - include: [ - { - model: models.Thread, - required: true, - }, - ], - }); - mustExist('Comment', comment); - + body: async ({ payload, actor, auth }) => { + const { address, comment } = mustBeAuthorizedComment(actor, auth); const thread = comment.Thread!; - const address = await models.Address.findOne({ - where: { - community_id: thread.community_id, - user_id: actor.user.id, - address: actor.address, - }, - }); - mustExist('Community address', address); const calculated_voting_weight = await getVotingWeight( thread.community_id, diff --git a/libs/model/src/comment/UpdateComment.command.ts b/libs/model/src/comment/UpdateComment.command.ts index a0e706e1882..d7033940731 100644 --- a/libs/model/src/comment/UpdateComment.command.ts +++ b/libs/model/src/comment/UpdateComment.command.ts @@ -1,8 +1,8 @@ -import { type Command } from '@hicommonwealth/core'; +import { InvalidInput, type Command } from '@hicommonwealth/core'; import * as schemas from '@hicommonwealth/schemas'; import { models } from '../database'; import { isAuthorized, type AuthContext } from '../middleware'; -import { mustExist } from '../middleware/guards'; +import { mustBeAuthorized } from '../middleware/guards'; import { emitMentions, findMentionDiff, @@ -19,31 +19,24 @@ export function UpdateComment(): Command< return { ...schemas.UpdateComment, auth: [isAuthorized({})], - body: async ({ actor, payload }) => { + body: async ({ actor, payload, auth }) => { + const { address } = mustBeAuthorized(actor, auth); const { comment_id, discord_meta } = payload; + // find by comment_id or discord_meta const comment = await models.Comment.findOne({ where: comment_id ? { id: comment_id } : { discord_meta }, include: [{ model: models.Thread, required: true }], }); - mustExist('Comment', comment); - const thread = comment.Thread!; + if (!comment) throw new InvalidInput('Comment not found'); + const thread = comment.Thread!; const currentVersion = await models.CommentVersionHistory.findOne({ where: { comment_id: comment.id }, order: [['timestamp', 'DESC']], }); if (currentVersion?.text !== payload.text) { - const address = await models.Address.findOne({ - where: { - community_id: thread.community_id, - user_id: actor.user.id, - address: actor.address, - }, - }); - mustExist('Community address', address); - const text = sanitizeQuillText(payload.text); const plaintext = quillToPlain(text); const mentions = findMentionDiff( diff --git a/libs/model/src/middleware/guards.ts b/libs/model/src/middleware/guards.ts index 1321b5a4c14..8886316b784 100644 --- a/libs/model/src/middleware/guards.ts +++ b/libs/model/src/middleware/guards.ts @@ -4,6 +4,8 @@ import { InvalidState, logger, } from '@hicommonwealth/core'; +import { AddressInstance, ThreadInstance } from '../models'; +import { AuthContext } from './authorization'; const log = logger(import.meta); @@ -56,3 +58,41 @@ export function mustBeSuperAdmin(actor: Actor) { if (!actor.user.isAdmin) throw new InvalidActor(actor, 'Must be super administrator'); } + +/** + * Address authorization guard + * @param auth auth context + * @returns narrowed auth context + */ +export function mustBeAuthorized(actor: Actor, auth?: AuthContext) { + if (!auth?.address) throw new InvalidActor(actor, 'Not authorized'); + return auth as AuthContext & { address: AddressInstance }; +} + +/** + * Thread authorization guard + * @param auth auth context + * @returns narrowed auth context + */ +export function mustBeAuthorizedThread(actor: Actor, auth?: AuthContext) { + if (!auth?.address) throw new InvalidActor(actor, 'Not authorized'); + if (!auth?.thread) throw new InvalidActor(actor, 'Not authorized thread'); + return auth as AuthContext & { + address: AddressInstance; + thread: ThreadInstance; + }; +} + +/** + * Comment authorization guard + * @param auth auth context + * @returns narrowed auth context + */ +export function mustBeAuthorizedComment(actor: Actor, auth?: AuthContext) { + if (!auth?.address) throw new InvalidActor(actor, 'Not authorized'); + if (!auth?.comment) throw new InvalidActor(actor, 'Not authorized comment'); + return auth as AuthContext & { + address: AddressInstance; + comment: ThreadInstance; + }; +} diff --git a/libs/model/src/thread/CreateThread.command.ts b/libs/model/src/thread/CreateThread.command.ts index 2893af3c9e3..62101c10fe5 100644 --- a/libs/model/src/thread/CreateThread.command.ts +++ b/libs/model/src/thread/CreateThread.command.ts @@ -13,7 +13,7 @@ import { GetActiveContestManagers } from '../contest'; import { models } from '../database'; import { isAuthorized, type AuthContext } from '../middleware'; import { verifyThreadSignature } from '../middleware/canvas'; -import { mustExist } from '../middleware/guards'; +import { mustBeAuthorized } from '../middleware/guards'; import { tokenBalanceCache } from '../services'; import { emitMentions, @@ -90,7 +90,9 @@ export function CreateThread(): Command< isAuthorized({ action: schemas.PermissionEnum.CREATE_THREAD }), verifyThreadSignature, ], - body: async ({ actor, payload }) => { + body: async ({ actor, payload, auth }) => { + const { address } = mustBeAuthorized(actor, auth); + const { community_id, topic_id, kind, url, ...rest } = payload; if (kind === 'link' && !url?.trim()) @@ -109,16 +111,6 @@ export function CreateThread(): Command< checkContestLimits(activeContestManagers, actor.address!); } - // Loading to update last_active - const address = await models.Address.findOne({ - where: { - user_id: actor.user.id, - community_id, - address: actor.address, - }, - }); - mustExist('Community address', address); - const body = sanitizeQuillText(payload.body); const plaintext = kind === 'discussion' ? quillToPlain(body) : body; const mentions = uniqueMentions(parseUserMentions(body)); diff --git a/libs/model/src/thread/CreateThreadReaction.command.ts b/libs/model/src/thread/CreateThreadReaction.command.ts index 5718d294d9d..0450433aa1b 100644 --- a/libs/model/src/thread/CreateThreadReaction.command.ts +++ b/libs/model/src/thread/CreateThreadReaction.command.ts @@ -3,7 +3,7 @@ import * as schemas from '@hicommonwealth/schemas'; import { models } from '../database'; import { isAuthorized, type AuthContext } from '../middleware'; import { verifyReactionSignature } from '../middleware/canvas'; -import { mustExist } from '../middleware/guards'; +import { mustBeAuthorizedThread } from '../middleware/guards'; import { getVotingWeight } from '../services/stakeHelper'; export const CreateThreadReactionErrors = { @@ -22,23 +22,12 @@ export function CreateThreadReaction(): Command< }), verifyReactionSignature, ], - body: async ({ actor, payload }) => { - const thread = await models.Thread.findOne({ - where: { id: payload.thread_id }, - }); - mustExist('Thread', thread); + body: async ({ payload, actor, auth }) => { + const { address, thread } = mustBeAuthorizedThread(actor, auth); + if (thread.archived_at) throw new InvalidState(CreateThreadReactionErrors.ThreadArchived); - const address = await models.Address.findOne({ - where: { - user_id: actor.user.id, - community_id: thread.community_id, - address: actor.address, - }, - }); - mustExist('Community address', address); - const calculated_voting_weight = await getVotingWeight( thread.community_id, address.address, diff --git a/libs/schemas/src/commands/comment.schemas.ts b/libs/schemas/src/commands/comment.schemas.ts index a3ebbcc8506..813a171833a 100644 --- a/libs/schemas/src/commands/comment.schemas.ts +++ b/libs/schemas/src/commands/comment.schemas.ts @@ -21,6 +21,9 @@ export const UpdateComment = { input: z.object({ comment_id: PG_INT, text: z.string().trim().min(1), + + // discord integration + thread_id: PG_INT.optional(), discord_meta: DiscordMetaSchema.optional(), }), output: Comment.extend({ community_id: z.string() }), diff --git a/libs/shared/src/commonProtocol/chainConfig.ts b/libs/shared/src/commonProtocol/chainConfig.ts index 05a07eb3fe1..514c676f331 100644 --- a/libs/shared/src/commonProtocol/chainConfig.ts +++ b/libs/shared/src/commonProtocol/chainConfig.ts @@ -4,6 +4,10 @@ export enum ValidChains { SepoliaBase = 84532, Sepolia = 11155111, Blast = 81457, + Linea = 59144, + Optimism = 10, + Mainnet = 1, + Arbitrum = 42161, } export const STAKE_ID = 2; @@ -39,4 +43,24 @@ export const factoryContracts: { communityStake: '0xcc752fd15A7Dd0d5301b6A626316E7211352Cf62', chainId: 8453, }, + [ValidChains.Linea]: { + factory: '0xe3ae9569f4523161742414480f87967e991741bd', + communityStake: '0xcc752fd15a7dd0d5301b6a626316e7211352cf62', + chainId: 59144, + }, + [ValidChains.Optimism]: { + factory: '0xe3ae9569f4523161742414480f87967e991741bd', + communityStake: '0xcc752fd15a7dd0d5301b6a626316e7211352cf62', + chainId: 10, + }, + [ValidChains.Mainnet]: { + factory: '0x90aa47bf6e754f69ee53f05b5187b320e3118b0f', + communityStake: '0x9ed281e62db1b1d98af90106974891a4c1ca3a47', + chainId: 1, + }, + [ValidChains.Arbitrum]: { + factory: '0xE3AE9569f4523161742414480f87967e991741bd', + communityStake: '0xcc752fd15A7Dd0d5301b6A626316E7211352Cf62', + chainId: 42161, + }, }; 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( diff --git a/packages/commonwealth/server/migrations/20240904153821-new-chain-event-sources.js b/packages/commonwealth/server/migrations/20240904153821-new-chain-event-sources.js new file mode 100644 index 00000000000..bfecb2dbdfc --- /dev/null +++ b/packages/commonwealth/server/migrations/20240904153821-new-chain-event-sources.js @@ -0,0 +1,148 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.sequelize.transaction(async (transaction) => { + const specificEvmChainIds = [59144, 10, 42161, 1]; + + const chainNodes = await queryInterface.sequelize.query( + 'SELECT id, eth_chain_id FROM "ChainNodes" WHERE eth_chain_id IN (:evmChainIds)', + { + replacements: { evmChainIds: specificEvmChainIds }, + type: Sequelize.QueryTypes.SELECT, + transaction, + }, + ); + + const contractAbis = await queryInterface.sequelize.query( + 'SELECT id, nickname FROM "ContractAbis" WHERE nickname IN (:nicknames)', + { + replacements: { nicknames: ['NamespaceFactory', 'CommunityStakes'] }, + type: Sequelize.QueryTypes.SELECT, + transaction, + }, + ); + + const abiIds = { + NamespaceFactory: contractAbis.find( + (abi) => abi.nickname === 'NamespaceFactory', + ).id, + CommunityStakes: contractAbis.find( + (abi) => abi.nickname === 'CommunityStakes', + ).id, + }; + + const hardCodedValueSetsL2 = [ + { + contract_address: '0xedf43C919f59900C82d963E99d822dA3F95575EA', + event_signature: + '0x8870ba2202802ce285ce6bead5ac915b6dc2d35c8a9d6f96fa56de9de12829d5', + kind: 'DeployedNamespace', + abi_id: abiIds.NamespaceFactory, + active: true, + }, + { + contract_address: '0xcc752fd15A7Dd0d5301b6A626316E7211352Cf62', + event_signature: + '0xfc13c9a8a9a619ac78b803aecb26abdd009182411d51a986090f82519d88a89e', + kind: 'Trade', + abi_id: abiIds.CommunityStakes, + active: true, + }, + { + contract_address: '0xedf43C919f59900C82d963E99d822dA3F95575EA', + event_signature: + '0x990f533044dbc89b838acde9cd2c72c400999871cf8f792d731edcae15ead693', + kind: 'NewContest', + abi_id: abiIds.NamespaceFactory, + active: true, + }, + ]; + + const hardCodedValueSetsETH = [ + { + contract_address: '0x90aa47bf6e754f69ee53f05b5187b320e3118b0f', + event_signature: + '0x8870ba2202802ce285ce6bead5ac915b6dc2d35c8a9d6f96fa56de9de12829d5', + kind: 'DeployedNamespace', + abi_id: abiIds.NamespaceFactory, + active: true, + }, + { + contract_address: '0x9ed281e62db1b1d98af90106974891a4c1ca3a47', + event_signature: + '0xfc13c9a8a9a619ac78b803aecb26abdd009182411d51a986090f82519d88a89e', + kind: 'Trade', + abi_id: abiIds.CommunityStakes, + active: true, + }, + { + contract_address: '0x90aa47bf6e754f69ee53f05b5187b320e3118b0f', + event_signature: + '0x990f533044dbc89b838acde9cd2c72c400999871cf8f792d731edcae15ead693', + kind: 'NewContest', + abi_id: abiIds.NamespaceFactory, + active: true, + }, + ]; + + const records = chainNodes.flatMap((node) => { + if (node.eth_chain_id == 1) { + return hardCodedValueSetsETH.map((valueSet) => ({ + chain_node_id: node.id, + ...valueSet, + })); + } else { + return hardCodedValueSetsL2.map((valueSet) => ({ + chain_node_id: node.id, + ...valueSet, + })); + } + }); + + await queryInterface.bulkInsert('EvmEventSources', records, { + transaction, + }); + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.sequelize.transaction(async (transaction) => { + const chainNodes = await queryInterface.sequelize.query( + 'SELECT id, eth_chain_id FROM "ChainNodes" WHERE eth_chain_id IN (:evmChainIds)', + { + replacements: { evmChainIds: [59144, 10, 42161, 1] }, + type: Sequelize.QueryTypes.SELECT, + transaction, + }, + ); + + const chainNodeIds = chainNodes.map((node) => node.id); + + const contractAddressesL2 = [ + '0xedf43C919f59900C82d963E99d822dA3F95575EA', + '0xcc752fd15A7Dd0d5301b6A626316E7211352Cf62', + '0xedf43C919f59900C82d963E99d822dA3F95575EA', + ]; + + const contractAddressesETH = [ + '0x90aa47bf6e754f69ee53f05b5187b320e3118b0f', + '0x9ed281e62db1b1d98af90106974891a4c1ca3a47', + '0x90aa47bf6e754f69ee53f05b5187b320e3118b0f', + ]; + + await queryInterface.bulkDelete( + 'EvmEventSources', + { + chain_node_id: chainNodeIds, + [Sequelize.Op.or]: [ + { contract_address: contractAddressesL2 }, + { contract_address: contractAddressesETH }, + ], + }, + { transaction }, + ); + }); + }, +}; diff --git a/packages/discord-bot/src/discord-consumer/handlers.ts b/packages/discord-bot/src/discord-consumer/handlers.ts index 2bd9535fff5..7d839a37eb6 100644 --- a/packages/discord-bot/src/discord-consumer/handlers.ts +++ b/packages/discord-bot/src/discord-consumer/handlers.ts @@ -88,7 +88,9 @@ export async function handleCommentMessages( case 'comment-update': await axios.patch(`${bot_path}/threads/${thread.id}/comments`, { ...sharedReqData, - body: encodeURIComponent(message.content), + comment_id: 0, // required in command + thread_id: thread.id, // to auth command + text: encodeURIComponent(message.content), }); break; case 'comment-delete':