From 3cdd790ae96e1260832d43eea1107272c03a069a Mon Sep 17 00:00:00 2001 From: Sinclair Chen Date: Tue, 11 Oct 2022 12:52:27 -0700 Subject: [PATCH] Autosave post, market, comment rich text (#1015) * Fix freezing when typing big docs * Make rich text fields autosave to localstorage * Add autosave for comments * delete vestigial text editor from challenges * Clear autosave on submit post/market/comment * lint --- .../challenges/create-challenge-modal.tsx | 3 -- web/components/comment-input.tsx | 16 +++++++--- .../contract/contract-description.tsx | 1 + web/components/create-post.tsx | 2 ++ web/components/editor.tsx | 31 ++++++++++++++++--- web/components/feed/feed-comments.tsx | 1 + web/pages/create.tsx | 6 ++-- web/posts/post-comments.tsx | 1 + 8 files changed, 45 insertions(+), 16 deletions(-) diff --git a/web/components/challenges/create-challenge-modal.tsx b/web/components/challenges/create-challenge-modal.tsx index f8d91a7b..367a8ed0 100644 --- a/web/components/challenges/create-challenge-modal.tsx +++ b/web/components/challenges/create-challenge-modal.tsx @@ -20,7 +20,6 @@ import { getProbability } from 'common/calculate' import { createMarket } from 'web/lib/firebase/api' import { removeUndefinedProps } from 'common/util/object' import { FIXED_ANTE } from 'common/economy' -import { useTextEditor } from 'web/components/editor' import { LoadingIndicator } from 'web/components/loading-indicator' import { track } from 'web/lib/service/analytics' import { CopyLinkButton } from '../copy-link-button' @@ -43,7 +42,6 @@ export function CreateChallengeModal(props: { const { user, contract, isOpen, setOpen } = props const [challengeSlug, setChallengeSlug] = useState('') const [loading, setLoading] = useState(false) - const { editor } = useTextEditor({ placeholder: '' }) return ( @@ -64,7 +62,6 @@ export function CreateChallengeModal(props: { question: newChallenge.question, outcomeType: 'BINARY', initialProb: 50, - description: editor?.getJSON(), ante: FIXED_ANTE, closeTime: dayjs().add(30, 'day').valueOf(), }) diff --git a/web/components/comment-input.tsx b/web/components/comment-input.tsx index 65a697fe..460fa438 100644 --- a/web/components/comment-input.tsx +++ b/web/components/comment-input.tsx @@ -17,13 +17,21 @@ export function CommentInput(props: { // Reply to another comment parentCommentId?: string onSubmitComment?: (editor: Editor) => void + // unique id for autosave + pageId: string className?: string }) { - const { parentAnswerOutcome, parentCommentId, replyTo, onSubmitComment } = - props + const { + parentAnswerOutcome, + parentCommentId, + replyTo, + onSubmitComment, + pageId, + } = props const user = useUser() const { editor, upload } = useTextEditor({ + key: `comment ${pageId} ${parentCommentId ?? parentAnswerOutcome ?? ''}`, simple: true, max: MAX_COMMENT_LENGTH, placeholder: @@ -80,7 +88,7 @@ export function CommentInputTextArea(props: { const submit = () => { submitComment() - editor?.commands?.clearContent() + editor?.commands?.clearContent(true) } useEffect(() => { @@ -107,7 +115,7 @@ export function CommentInputTextArea(props: { }, }) // insert at mention and focus - if (replyTo) { + if (replyTo && editor.isEmpty) { editor .chain() .setContent({ diff --git a/web/components/contract/contract-description.tsx b/web/components/contract/contract-description.tsx index 40532c21..855bc750 100644 --- a/web/components/contract/contract-description.tsx +++ b/web/components/contract/contract-description.tsx @@ -48,6 +48,7 @@ function RichEditContract(props: { contract: Contract; isAdmin?: boolean }) { const [isSubmitting, setIsSubmitting] = useState(false) const { editor, upload } = useTextEditor({ + // key: `description ${contract.id}`, max: MAX_DESCRIPTION_LENGTH, defaultValue: contract.description, disabled: isSubmitting, diff --git a/web/components/create-post.tsx b/web/components/create-post.tsx index f7d9b8bd..a4b65661 100644 --- a/web/components/create-post.tsx +++ b/web/components/create-post.tsx @@ -21,6 +21,7 @@ export function CreatePost(props: { group?: Group }) { const { group } = props const { editor, upload } = useTextEditor({ + key: `post ${group?.id || ''}`, disabled: isSubmitting, }) @@ -45,6 +46,7 @@ export function CreatePost(props: { group?: Group }) { return e }) if (result.post) { + editor.commands.clearContent(true) await Router.push(postPath(result.post.slug)) } } diff --git a/web/components/editor.tsx b/web/components/editor.tsx index 17cabc38..f0b6f4bb 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -14,7 +14,7 @@ import StarterKit from '@tiptap/starter-kit' import { Image } from '@tiptap/extension-image' import { Link } from '@tiptap/extension-link' import clsx from 'clsx' -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { Linkify } from './linkify' import { uploadImage } from 'web/lib/firebase/storage' import { useMutation } from 'react-query' @@ -41,6 +41,12 @@ import ItalicIcon from 'web/lib/icons/italic-icon' import LinkIcon from 'web/lib/icons/link-icon' import { getUrl } from 'common/util/parse' import { TiptapSpoiler } from 'common/util/tiptap-spoiler' +import { + storageStore, + usePersistentState, +} from 'web/hooks/use-persistent-state' +import { safeLocalStorage } from 'web/lib/util/local' +import { debounce } from 'lodash' const DisplayImage = Image.configure({ HTMLAttributes: { @@ -90,19 +96,34 @@ export function useTextEditor(props: { defaultValue?: Content disabled?: boolean simple?: boolean + key?: string // unique key for autosave. If set, plz call `clearContent(true)` on submit to clear autosave }) { - const { placeholder, max, defaultValue = '', disabled, simple } = props + const { placeholder, max, defaultValue, disabled, simple, key } = props + + const [content, saveContent] = usePersistentState( + undefined, + { + key: `text ${key}`, + store: storageStore(safeLocalStorage()), + } + ) + + // eslint-disable-next-line react-hooks/exhaustive-deps + const save = useCallback(debounce(saveContent, 500), []) const editorClass = clsx( proseClass, !simple && 'min-h-[6em]', 'outline-none pt-2 px-4', 'prose-img:select-auto', - '[&_.ProseMirror-selectednode]:outline-dotted [&_*]:outline-indigo-300' // selected img, emebeds + '[&_.ProseMirror-selectednode]:outline-dotted [&_*]:outline-indigo-300' // selected img, embeds ) const editor = useEditor({ - editorProps: { attributes: { class: editorClass } }, + editorProps: { + attributes: { class: editorClass, spellcheck: simple ? 'true' : 'false' }, + }, + onUpdate: key ? ({ editor }) => save(editor.getJSON()) : undefined, extensions: [ ...editorExtensions(simple), Placeholder.configure({ @@ -112,7 +133,7 @@ export function useTextEditor(props: { }), CharacterCount.configure({ limit: max }), ], - content: defaultValue, + content: defaultValue ?? (key && content ? content : ''), }) const upload = useUploadMutation(editor) diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index 0cc7012d..552bfe7c 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -268,6 +268,7 @@ export function ContractCommentInput(props: { parentAnswerOutcome={parentAnswerOutcome} parentCommentId={parentCommentId} onSubmitComment={onSubmitComment} + pageId={contract.id} className={className} /> ) diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 7ba99a39..ca6c5ffe 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -22,7 +22,6 @@ import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' import { getGroup, groupPath } from 'web/lib/firebase/groups' import { Group } from 'common/group' import { useTracking } from 'web/hooks/use-tracking' -import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes' import { track } from 'web/lib/service/analytics' import { GroupSelector } from 'web/components/groups/group-selector' import { User } from 'common/user' @@ -228,6 +227,7 @@ export function NewContract(props: { : `e.g. I will choose the answer according to...` const { editor, upload } = useTextEditor({ + key: 'create market', max: MAX_DESCRIPTION_LENGTH, placeholder: descriptionPlaceholder, disabled: isSubmitting, @@ -236,9 +236,6 @@ export function NewContract(props: { : undefined, }) - const isEditorFilled = editor != null && !editor.isEmpty - useWarnUnsavedChanges(!isSubmitting && (Boolean(question) || isEditorFilled)) - function setCloseDateInDays(days: number) { const newCloseDate = dayjs().add(days, 'day').format('YYYY-MM-DD') setCloseDate(newCloseDate) @@ -272,6 +269,7 @@ export function NewContract(props: { selectedGroup: selectedGroup?.id, isFree: false, }) + editor?.commands.clearContent(true) await router.push(contractPath(result as Contract)) } catch (e) { console.error('error creating contract', e, (e as any).details) diff --git a/web/posts/post-comments.tsx b/web/posts/post-comments.tsx index 62f69074..722c16c6 100644 --- a/web/posts/post-comments.tsx +++ b/web/posts/post-comments.tsx @@ -94,6 +94,7 @@ export function PostCommentInput(props: { replyTo={replyToUser} parentCommentId={parentCommentId} onSubmitComment={onSubmitComment} + pageId={post.id} /> ) }