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 { createMarket } from 'web/lib/firebase/api'
import { removeUndefinedProps } from 'common/util/object' import { removeUndefinedProps } from 'common/util/object'
import { FIXED_ANTE } from 'common/economy' import { FIXED_ANTE } from 'common/economy'
import { useTextEditor } from 'web/components/editor'
import { LoadingIndicator } from 'web/components/loading-indicator' import { LoadingIndicator } from 'web/components/loading-indicator'
import { track } from 'web/lib/service/analytics' import { track } from 'web/lib/service/analytics'
import { CopyLinkButton } from '../copy-link-button' import { CopyLinkButton } from '../copy-link-button'
@ -43,7 +42,6 @@ export function CreateChallengeModal(props: {
const { user, contract, isOpen, setOpen } = props const { user, contract, isOpen, setOpen } = props
const [challengeSlug, setChallengeSlug] = useState('') const [challengeSlug, setChallengeSlug] = useState('')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const { editor } = useTextEditor({ placeholder: '' })
return ( return (
<Modal open={isOpen} setOpen={setOpen}> <Modal open={isOpen} setOpen={setOpen}>
@ -64,7 +62,6 @@ export function CreateChallengeModal(props: {
question: newChallenge.question, question: newChallenge.question,
outcomeType: 'BINARY', outcomeType: 'BINARY',
initialProb: 50, initialProb: 50,
description: editor?.getJSON(),
ante: FIXED_ANTE, ante: FIXED_ANTE,
closeTime: dayjs().add(30, 'day').valueOf(), closeTime: dayjs().add(30, 'day').valueOf(),
}) })

View File

@ -17,13 +17,21 @@ export function CommentInput(props: {
// Reply to another comment // Reply to another comment
parentCommentId?: string parentCommentId?: string
onSubmitComment?: (editor: Editor) => void onSubmitComment?: (editor: Editor) => void
// unique id for autosave
pageId: string
className?: string className?: string
}) { }) {
const { parentAnswerOutcome, parentCommentId, replyTo, onSubmitComment } = const {
props parentAnswerOutcome,
parentCommentId,
replyTo,
onSubmitComment,
pageId,
} = props
const user = useUser() const user = useUser()
const { editor, upload } = useTextEditor({ const { editor, upload } = useTextEditor({
key: `comment ${pageId} ${parentCommentId ?? parentAnswerOutcome ?? ''}`,
simple: true, simple: true,
max: MAX_COMMENT_LENGTH, max: MAX_COMMENT_LENGTH,
placeholder: placeholder:
@ -80,7 +88,7 @@ export function CommentInputTextArea(props: {
const submit = () => { const submit = () => {
submitComment() submitComment()
editor?.commands?.clearContent() editor?.commands?.clearContent(true)
} }
useEffect(() => { useEffect(() => {
@ -107,7 +115,7 @@ export function CommentInputTextArea(props: {
}, },
}) })
// insert at mention and focus // insert at mention and focus
if (replyTo) { if (replyTo && editor.isEmpty) {
editor editor
.chain() .chain()
.setContent({ .setContent({

View File

@ -48,6 +48,7 @@ function RichEditContract(props: { contract: Contract; isAdmin?: boolean }) {
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const { editor, upload } = useTextEditor({ const { editor, upload } = useTextEditor({
// key: `description ${contract.id}`,
max: MAX_DESCRIPTION_LENGTH, max: MAX_DESCRIPTION_LENGTH,
defaultValue: contract.description, defaultValue: contract.description,
disabled: isSubmitting, disabled: isSubmitting,

View File

@ -21,6 +21,7 @@ export function CreatePost(props: { group?: Group }) {
const { group } = props const { group } = props
const { editor, upload } = useTextEditor({ const { editor, upload } = useTextEditor({
key: `post ${group?.id || ''}`,
disabled: isSubmitting, disabled: isSubmitting,
}) })
@ -45,6 +46,7 @@ export function CreatePost(props: { group?: Group }) {
return e return e
}) })
if (result.post) { if (result.post) {
editor.commands.clearContent(true)
await Router.push(postPath(result.post.slug)) 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 { Image } from '@tiptap/extension-image'
import { Link } from '@tiptap/extension-link' import { Link } from '@tiptap/extension-link'
import clsx from 'clsx' import clsx from 'clsx'
import { useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { Linkify } from './linkify' import { Linkify } from './linkify'
import { uploadImage } from 'web/lib/firebase/storage' import { uploadImage } from 'web/lib/firebase/storage'
import { useMutation } from 'react-query' 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 LinkIcon from 'web/lib/icons/link-icon'
import { getUrl } from 'common/util/parse' import { getUrl } from 'common/util/parse'
import { TiptapSpoiler } from 'common/util/tiptap-spoiler' 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({ const DisplayImage = Image.configure({
HTMLAttributes: { HTMLAttributes: {
@ -90,19 +96,34 @@ export function useTextEditor(props: {
defaultValue?: Content defaultValue?: Content
disabled?: boolean disabled?: boolean
simple?: 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( const editorClass = clsx(
proseClass, proseClass,
!simple && 'min-h-[6em]', !simple && 'min-h-[6em]',
'outline-none pt-2 px-4', 'outline-none pt-2 px-4',
'prose-img:select-auto', '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({ const editor = useEditor({
editorProps: { attributes: { class: editorClass } }, editorProps: {
attributes: { class: editorClass, spellcheck: simple ? 'true' : 'false' },
},
onUpdate: key ? ({ editor }) => save(editor.getJSON()) : undefined,
extensions: [ extensions: [
...editorExtensions(simple), ...editorExtensions(simple),
Placeholder.configure({ Placeholder.configure({
@ -112,7 +133,7 @@ export function useTextEditor(props: {
}), }),
CharacterCount.configure({ limit: max }), CharacterCount.configure({ limit: max }),
], ],
content: defaultValue, content: defaultValue ?? (key && content ? content : ''),
}) })
const upload = useUploadMutation(editor) const upload = useUploadMutation(editor)

View File

@ -268,6 +268,7 @@ export function ContractCommentInput(props: {
parentAnswerOutcome={parentAnswerOutcome} parentAnswerOutcome={parentAnswerOutcome}
parentCommentId={parentCommentId} parentCommentId={parentCommentId}
onSubmitComment={onSubmitComment} onSubmitComment={onSubmitComment}
pageId={contract.id}
className={className} 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 { getGroup, groupPath } from 'web/lib/firebase/groups'
import { Group } from 'common/group' import { Group } from 'common/group'
import { useTracking } from 'web/hooks/use-tracking' import { useTracking } from 'web/hooks/use-tracking'
import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes'
import { track } from 'web/lib/service/analytics' import { track } from 'web/lib/service/analytics'
import { GroupSelector } from 'web/components/groups/group-selector' import { GroupSelector } from 'web/components/groups/group-selector'
import { User } from 'common/user' import { User } from 'common/user'
@ -228,6 +227,7 @@ export function NewContract(props: {
: `e.g. I will choose the answer according to...` : `e.g. I will choose the answer according to...`
const { editor, upload } = useTextEditor({ const { editor, upload } = useTextEditor({
key: 'create market',
max: MAX_DESCRIPTION_LENGTH, max: MAX_DESCRIPTION_LENGTH,
placeholder: descriptionPlaceholder, placeholder: descriptionPlaceholder,
disabled: isSubmitting, disabled: isSubmitting,
@ -236,9 +236,6 @@ export function NewContract(props: {
: undefined, : undefined,
}) })
const isEditorFilled = editor != null && !editor.isEmpty
useWarnUnsavedChanges(!isSubmitting && (Boolean(question) || isEditorFilled))
function setCloseDateInDays(days: number) { function setCloseDateInDays(days: number) {
const newCloseDate = dayjs().add(days, 'day').format('YYYY-MM-DD') const newCloseDate = dayjs().add(days, 'day').format('YYYY-MM-DD')
setCloseDate(newCloseDate) setCloseDate(newCloseDate)
@ -272,6 +269,7 @@ export function NewContract(props: {
selectedGroup: selectedGroup?.id, selectedGroup: selectedGroup?.id,
isFree: false, isFree: false,
}) })
editor?.commands.clearContent(true)
await router.push(contractPath(result as Contract)) await router.push(contractPath(result as Contract))
} catch (e) { } catch (e) {
console.error('error creating contract', e, (e as any).details) console.error('error creating contract', e, (e as any).details)

View File

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