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
This commit is contained in:
Sinclair Chen 2022-10-11 12:52:27 -07:00 committed by GitHub
parent 6c1ac89cbe
commit 3cdd790ae9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 45 additions and 16 deletions

View File

@ -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 (
<Modal open={isOpen} setOpen={setOpen}>
@ -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(),
})

View File

@ -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({

View File

@ -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,

View File

@ -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))
}
}

View File

@ -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<JSONContent | undefined>(
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)

View File

@ -268,6 +268,7 @@ export function ContractCommentInput(props: {
parentAnswerOutcome={parentAnswerOutcome}
parentCommentId={parentCommentId}
onSubmitComment={onSubmitComment}
pageId={contract.id}
className={className}
/>
)

View File

@ -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)

View File

@ -94,6 +94,7 @@ export function PostCommentInput(props: {
replyTo={replyToUser}
parentCommentId={parentCommentId}
onSubmitComment={onSubmitComment}
pageId={post.id}
/>
)
}