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:
parent
6c1ac89cbe
commit
3cdd790ae9
|
@ -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(),
|
||||||
})
|
})
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -94,6 +94,7 @@ export function PostCommentInput(props: {
|
||||||
replyTo={replyToUser}
|
replyTo={replyToUser}
|
||||||
parentCommentId={parentCommentId}
|
parentCommentId={parentCommentId}
|
||||||
onSubmitComment={onSubmitComment}
|
onSubmitComment={onSubmitComment}
|
||||||
|
pageId={post.id}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user