diff --git a/common/util/parse.ts b/common/util/parse.ts index 21cf1760..acbf6c61 100644 --- a/common/util/parse.ts +++ b/common/util/parse.ts @@ -1,6 +1,23 @@ import { MAX_TAG_LENGTH } from '../contract' -import { generateText, JSONContent, Extension } from '@tiptap/core' -import * as StarterKit from '@tiptap/starter-kit' // needed for cjs import to work on firebase +import { generateText, JSONContent } from '@tiptap/core' +// Tiptap starter extensions +import { Blockquote } from '@tiptap/extension-blockquote' +import { Bold } from '@tiptap/extension-bold' +import { BulletList } from '@tiptap/extension-bullet-list' +import { Code } from '@tiptap/extension-code' +import { CodeBlock } from '@tiptap/extension-code-block' +import { Document } from '@tiptap/extension-document' +import { HardBreak } from '@tiptap/extension-hard-break' +import { Heading } from '@tiptap/extension-heading' +import { History } from '@tiptap/extension-history' +import { HorizontalRule } from '@tiptap/extension-horizontal-rule' +import { Italic } from '@tiptap/extension-italic' +import { ListItem } from '@tiptap/extension-list-item' +import { OrderedList } from '@tiptap/extension-ordered-list' +import { Paragraph } from '@tiptap/extension-paragraph' +import { Strike } from '@tiptap/extension-strike' +import { Text } from '@tiptap/extension-text' +// import * as StarterKit from '@tiptap/starter-kit' import { Image } from '@tiptap/extension-image' export function parseTags(text: string) { @@ -31,8 +48,30 @@ export function parseWordsAsTags(text: string) { return parseTags(taggedText) } -export function richTextToString(text: JSONContent | string) { - return typeof text === 'string' - ? text - : generateText(text, [StarterKit as unknown as Extension, Image]) +// can't just do [StarterKit, Image...] because it doesn't work with some imports +// see +export const exhibitExts = [ + Blockquote, + Bold, + BulletList, + Code, + CodeBlock, + Document, + HardBreak, + Heading, + History, + HorizontalRule, + Italic, + ListItem, + OrderedList, + Paragraph, + Strike, + Text, + + Image, +] +// export const exhibitExts = [StarterKit as unknown as Extension, Image] + +export function richTextToString(text?: JSONContent) { + return !text ? '' : generateText(text, exhibitExts) } diff --git a/functions/src/on-create-contract.ts b/functions/src/on-create-contract.ts index 614a5c2a..28682793 100644 --- a/functions/src/on-create-contract.ts +++ b/functions/src/on-create-contract.ts @@ -3,6 +3,7 @@ import { getUser } from './utils' import { createNotification } from './create-notification' import { Contract } from '../../common/contract' import { richTextToString } from '../../common/util/parse' +import { JSONContent } from '@tiptap/core' export const onCreateContract = functions.firestore .document('contracts/{contractId}') @@ -19,7 +20,7 @@ export const onCreateContract = functions.firestore 'created', contractCreator, eventId, - richTextToString(contract.description), + richTextToString(contract.description as JSONContent), contract ) }) diff --git a/web/components/contract/contract-description.tsx b/web/components/contract/contract-description.tsx index 2c723c97..b932c41d 100644 --- a/web/components/contract/contract-description.tsx +++ b/web/components/contract/contract-description.tsx @@ -5,12 +5,13 @@ import Textarea from 'react-expanding-textarea' import { CATEGORY_LIST } from '../../../common/categories' import { Contract } from 'common/contract' -import { parseTags, richTextToString } from 'common/util/parse' +import { parseTags, exhibitExts } from 'common/util/parse' import { useAdmin } from 'web/hooks/use-admin' import { updateContract } from 'web/lib/firebase/contracts' import { Row } from '../layout/row' import { TagsList } from '../tags-list' import { Content } from '../editor' +import { Editor } from '@tiptap/react' export function ContractDescription(props: { contract: Contract @@ -21,24 +22,33 @@ export function ContractDescription(props: { const descriptionTimestamp = () => `${dayjs().format('MMM D, h:mma')}: ` const isAdmin = useAdmin() + const desc = contract.description ?? '' + // Append the new description (after a newline) async function saveDescription(newText: string) { - // TODO: implement appending rich text description - const textDescription = richTextToString(contract.description) - const newDescription = `${textDescription}\n\n${newText}`.trim() + console.log(desc, exhibitExts) + + const editor = new Editor({ content: desc, extensions: exhibitExts }) + editor + .chain() + .focus('end') + .insertContent('

') + .insertContent(newText.trim()) + .run() + const tags = parseTags( - `${newDescription} ${contract.tags.map((tag) => `#${tag}`).join(' ')}` + `${editor.getText()} ${contract.tags.map((tag) => `#${tag}`).join(' ')}` ) const lowercaseTags = tags.map((tag) => tag.toLowerCase()) await updateContract(contract.id, { - description: newDescription, + description: editor.getJSON(), tags, lowercaseTags, }) } - if (!isCreator && !contract.description) return null + if (!isCreator) return null const { tags } = contract const categories = tags.filter((tag) => @@ -52,7 +62,7 @@ export function ContractDescription(props: { className )} > - + {categories.length > 0 && (
diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index f908918e..b4d67520 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -31,6 +31,8 @@ import { DAY_MS } from 'common/util/time' import { useGroupsWithContract } from 'web/hooks/use-group' import { ShareIconButton } from 'web/components/share-icon-button' import { useUser } from 'web/hooks/use-user' +import { Editor } from '@tiptap/react' +import { exhibitExts } from 'common/util/parse' export type ShowTime = 'resolve-date' | 'close-date' @@ -268,13 +270,20 @@ function EditableCloseDate(props: { const newCloseTime = dayjs(closeDate).valueOf() if (newCloseTime === closeTime) setIsEditingCloseTime(false) else if (newCloseTime > Date.now()) { - const { description } = contract + const content = contract.description const formattedCloseDate = dayjs(newCloseTime).format('YYYY-MM-DD h:mm a') - const newDescription = `${description}\n\nClose date updated to ${formattedCloseDate}` + + const editor = new Editor({ content, extensions: exhibitExts }) + editor + .chain() + .focus('end') + .insertContent('

') + .insertContent(`Close date updated to ${formattedCloseDate}`) + .run() updateContract(contract.id, { closeTime: newCloseTime, - description: newDescription, + description: editor.getJSON(), }) setIsEditingCloseTime(false) diff --git a/web/components/editor.tsx b/web/components/editor.tsx index b86c261c..53f5fabf 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -8,6 +8,7 @@ import { useEffect } from 'react' import { Linkify } from './linkify' import { uploadImage } from 'web/lib/firebase/storage' import { useMutation } from 'react-query' +import { exhibitExts } from 'common/util/parse' const proseClass = 'prose prose-sm prose-p:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none' @@ -80,10 +81,12 @@ function RichContent(props: { content: JSONContent }) { const { content } = props const editor = useEditor({ editorProps: { attributes: { class: proseClass } }, - extensions: [StarterKit, Image], + extensions: exhibitExts, content, editable: false, }) + useEffect(() => void editor?.commands?.setContent(content), [editor, content]) + return } diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 57038560..f192f4d1 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -195,7 +195,7 @@ export function ContractPageContent( {ogCardProps && ( @@ -392,15 +392,18 @@ const getOpenGraphProps = (contract: Contract) => { creatorUsername, outcomeType, creatorAvatarUrl, + description: desc, } = contract const probPercent = outcomeType === 'BINARY' ? getBinaryProbPercent(contract) : undefined + const stringDesc = typeof desc === 'string' ? desc : richTextToString(desc) + const description = resolution - ? `Resolved ${resolution}. ${contract.description}` + ? `Resolved ${resolution}. ${stringDesc}` : probPercent - ? `${probPercent} chance. ${contract.description}` - : contract.description + ? `${probPercent} chance. ${stringDesc}` + : stringDesc return { question,