Quote reply support on text-based posts and comments (#526)
* Quote reply support on text-based posts and comments * Clean up the `onQuoteReply` prop usage * Refactor to use `useImperativeHandle` for Reply * quote selected text if any, otherwise quote whole item * Only quote selected text if it's from the item we're replying to, not just any selected text * add trailing newline to copied text * onPointerDown for mobile, quote+reply quotes text --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
This commit is contained in:
parent
362f95add9
commit
f6141a6965
|
@ -8,7 +8,7 @@ export default function ActionDropdown ({ children }) {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Dropdown className={`pointer ${styles.dropdown}`} as='span'>
|
<Dropdown className={`pointer ${styles.dropdown}`} as='span'>
|
||||||
<Dropdown.Toggle variant='success' as='a'>
|
<Dropdown.Toggle variant='success' as='a' onPointerDown={e => e.preventDefault()}>
|
||||||
<MoreIcon className='fill-grey ms-1' height={16} width={16} />
|
<MoreIcon className='fill-grey ms-1' height={16} width={16} />
|
||||||
</Dropdown.Toggle>
|
</Dropdown.Toggle>
|
||||||
<Dropdown.Menu>
|
<Dropdown.Menu>
|
||||||
|
|
|
@ -136,6 +136,8 @@ export default function Comment ({
|
||||||
? 'fwd'
|
? 'fwd'
|
||||||
: null
|
: null
|
||||||
const bountyPaid = root.bountyPaidTo?.includes(Number(item.id))
|
const bountyPaid = root.bountyPaidTo?.includes(Number(item.id))
|
||||||
|
const replyRef = useRef()
|
||||||
|
const contentContainerRef = useRef()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -165,6 +167,7 @@ export default function Comment ({
|
||||||
commentTextSingular='reply'
|
commentTextSingular='reply'
|
||||||
className={`${itemStyles.other} ${styles.other}`}
|
className={`${itemStyles.other} ${styles.other}`}
|
||||||
embellishUser={op && <><span> </span><Badge bg={op === 'fwd' ? 'secondary' : 'boost'} className={`${styles.op} bg-opacity-75`}>{op}</Badge></>}
|
embellishUser={op && <><span> </span><Badge bg={op === 'fwd' ? 'secondary' : 'boost'} className={`${styles.op} bg-opacity-75`}>{op}</Badge></>}
|
||||||
|
onQuoteReply={replyRef?.current?.quoteReply}
|
||||||
extraInfo={
|
extraInfo={
|
||||||
<>
|
<>
|
||||||
{includeParent && <Parent item={item} rootText={rootText} />}
|
{includeParent && <Parent item={item} rootText={rootText} />}
|
||||||
|
@ -207,7 +210,7 @@ export default function Comment ({
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<div className={styles.text}>
|
<div className={styles.text} ref={contentContainerRef}>
|
||||||
{item.searchText
|
{item.searchText
|
||||||
? <SearchText text={item.searchText} />
|
? <SearchText text={item.searchText} />
|
||||||
: (
|
: (
|
||||||
|
@ -224,7 +227,7 @@ export default function Comment ({
|
||||||
: (
|
: (
|
||||||
<div className={styles.children}>
|
<div className={styles.children}>
|
||||||
{!noReply &&
|
{!noReply &&
|
||||||
<Reply depth={depth + 1} item={item} replyOpen={replyOpen}>
|
<Reply depth={depth + 1} item={item} replyOpen={replyOpen} ref={replyRef} contentContainerRef={contentContainerRef}>
|
||||||
{root.bounty && !bountyPaid && <PayBounty item={item} />}
|
{root.bounty && !bountyPaid && <PayBounty item={item} />}
|
||||||
</Reply>}
|
</Reply>}
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -92,6 +92,7 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, setH
|
||||||
const [, meta, helpers] = useField(props)
|
const [, meta, helpers] = useField(props)
|
||||||
const [selectionRange, setSelectionRange] = useState({ start: 0, end: 0 })
|
const [selectionRange, setSelectionRange] = useState({ start: 0, end: 0 })
|
||||||
innerRef = innerRef || useRef(null)
|
innerRef = innerRef || useRef(null)
|
||||||
|
const previousTab = useRef(tab)
|
||||||
|
|
||||||
props.as ||= TextareaAutosize
|
props.as ||= TextareaAutosize
|
||||||
props.rows ||= props.minRows || 6
|
props.rows ||= props.minRows || 6
|
||||||
|
@ -100,6 +101,14 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, setH
|
||||||
!meta.value && setTab('write')
|
!meta.value && setTab('write')
|
||||||
}, [meta.value])
|
}, [meta.value])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// focus on input when switching to write tab from preview tab
|
||||||
|
if (innerRef?.current && tab === 'write' && previousTab?.current !== 'write') {
|
||||||
|
innerRef.current.focus()
|
||||||
|
}
|
||||||
|
previousTab.current = tab
|
||||||
|
}, [tab])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectionRange.start <= selectionRange.end && innerRef?.current) {
|
if (selectionRange.start <= selectionRange.end && innerRef?.current) {
|
||||||
const { start, end } = selectionRange
|
const { start, end } = selectionRange
|
||||||
|
@ -125,53 +134,49 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, setH
|
||||||
<Markdown width={18} height={18} />
|
<Markdown width={18} height={18} />
|
||||||
</a>
|
</a>
|
||||||
</Nav>
|
</Nav>
|
||||||
{tab === 'write'
|
<div className={tab === 'write' ? '' : 'd-none'}>
|
||||||
? (
|
<InputInner
|
||||||
<div>
|
{...props} onChange={(formik, e) => {
|
||||||
<InputInner
|
if (onChange) onChange(formik, e)
|
||||||
{...props} onChange={(formik, e) => {
|
if (setHasImgLink) {
|
||||||
if (onChange) onChange(formik, e)
|
setHasImgLink(mdHas(e.target.value, ['link', 'image']))
|
||||||
if (setHasImgLink) {
|
}
|
||||||
setHasImgLink(mdHas(e.target.value, ['link', 'image']))
|
}}
|
||||||
}
|
innerRef={innerRef}
|
||||||
}}
|
onKeyDown={(e) => {
|
||||||
innerRef={innerRef}
|
const metaOrCtrl = e.metaKey || e.ctrlKey
|
||||||
onKeyDown={(e) => {
|
if (metaOrCtrl) {
|
||||||
const metaOrCtrl = e.metaKey || e.ctrlKey
|
if (e.key === 'k') {
|
||||||
if (metaOrCtrl) {
|
// some browsers use CTRL+K to focus search bar so we have to prevent that behavior
|
||||||
if (e.key === 'k') {
|
e.preventDefault()
|
||||||
// some browsers use CTRL+K to focus search bar so we have to prevent that behavior
|
insertMarkdownLinkFormatting(innerRef.current, helpers.setValue, setSelectionRange)
|
||||||
e.preventDefault()
|
}
|
||||||
insertMarkdownLinkFormatting(innerRef.current, helpers.setValue, setSelectionRange)
|
if (e.key === 'b') {
|
||||||
}
|
// some browsers use CTRL+B to open bookmarks so we have to prevent that behavior
|
||||||
if (e.key === 'b') {
|
e.preventDefault()
|
||||||
// some browsers use CTRL+B to open bookmarks so we have to prevent that behavior
|
insertMarkdownBoldFormatting(innerRef.current, helpers.setValue, setSelectionRange)
|
||||||
e.preventDefault()
|
}
|
||||||
insertMarkdownBoldFormatting(innerRef.current, helpers.setValue, setSelectionRange)
|
if (e.key === 'i') {
|
||||||
}
|
// some browsers might use CTRL+I to do something else so prevent that behavior too
|
||||||
if (e.key === 'i') {
|
e.preventDefault()
|
||||||
// some browsers might use CTRL+I to do something else so prevent that behavior too
|
insertMarkdownItalicFormatting(innerRef.current, helpers.setValue, setSelectionRange)
|
||||||
e.preventDefault()
|
}
|
||||||
insertMarkdownItalicFormatting(innerRef.current, helpers.setValue, setSelectionRange)
|
if (e.key === 'Tab' && e.altKey) {
|
||||||
}
|
e.preventDefault()
|
||||||
if (e.key === 'Tab' && e.altKey) {
|
insertMarkdownTabFormatting(innerRef.current, helpers.setValue, setSelectionRange)
|
||||||
e.preventDefault()
|
}
|
||||||
insertMarkdownTabFormatting(innerRef.current, helpers.setValue, setSelectionRange)
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onKeyDown) onKeyDown(e)
|
if (onKeyDown) onKeyDown(e)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>)
|
</div>
|
||||||
: (
|
{tab !== 'write' &&
|
||||||
<div className='form-group'>
|
<div className='form-group'>
|
||||||
<div className={`${styles.text} form-control`}>
|
<div className={`${styles.text} form-control`}>
|
||||||
<Text topLevel={topLevel} noFragments tab={tab}>{meta.value}</Text>
|
<Text topLevel={topLevel} noFragments tab={tab}>{meta.value}</Text>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
)
|
)
|
||||||
|
@ -225,7 +230,8 @@ function FormGroup ({ className, label, children }) {
|
||||||
|
|
||||||
function InputInner ({
|
function InputInner ({
|
||||||
prepend, append, hint, showValid, onChange, onBlur, overrideValue,
|
prepend, append, hint, showValid, onChange, onBlur, overrideValue,
|
||||||
innerRef, noForm, clear, onKeyDown, inputGroupClassName, debounce, maxLength, ...props
|
innerRef, noForm, clear, onKeyDown, inputGroupClassName, debounce, maxLength,
|
||||||
|
...props
|
||||||
}) {
|
}) {
|
||||||
const [field, meta, helpers] = noForm ? [{}, {}, {}] : useField(props)
|
const [field, meta, helpers] = noForm ? [{}, {}, {}] : useField(props)
|
||||||
const formik = noForm ? null : useFormikContext()
|
const formik = noForm ? null : useFormikContext()
|
||||||
|
@ -483,7 +489,7 @@ export function Checkbox ({ children, label, groupClassName, hiddenLabel, extra,
|
||||||
const StorageKeyPrefixContext = createContext()
|
const StorageKeyPrefixContext = createContext()
|
||||||
|
|
||||||
export function Form ({
|
export function Form ({
|
||||||
initial, schema, onSubmit, children, initialError, validateImmediately, storageKeyPrefix, validateOnChange = true, invoiceable, ...props
|
initial, schema, onSubmit, children, initialError, validateImmediately, storageKeyPrefix, validateOnChange = true, invoiceable, innerRef, ...props
|
||||||
}) {
|
}) {
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const initialErrorToasted = useRef(false)
|
const initialErrorToasted = useRef(false)
|
||||||
|
@ -538,6 +544,7 @@ export function Form ({
|
||||||
toaster.danger(err.message || err.toString?.())
|
toaster.danger(err.message || err.toString?.())
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
innerRef={innerRef}
|
||||||
>
|
>
|
||||||
<FormikForm {...props} noValidate>
|
<FormikForm {...props} noValidate>
|
||||||
<StorageKeyPrefixContext.Provider value={storageKeyPrefix}>
|
<StorageKeyPrefixContext.Provider value={storageKeyPrefix}>
|
||||||
|
|
|
@ -13,7 +13,7 @@ import Button from 'react-bootstrap/Button'
|
||||||
import { TwitterTweetEmbed } from 'react-twitter-embed'
|
import { TwitterTweetEmbed } from 'react-twitter-embed'
|
||||||
import YouTube from 'react-youtube'
|
import YouTube from 'react-youtube'
|
||||||
import useDarkMode from './dark-mode'
|
import useDarkMode from './dark-mode'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import Poll from './poll'
|
import Poll from './poll'
|
||||||
import { commentsViewed } from '../lib/new-comments'
|
import { commentsViewed } from '../lib/new-comments'
|
||||||
import Related from './related'
|
import Related from './related'
|
||||||
|
@ -122,10 +122,13 @@ function FwdUsers ({ forwards }) {
|
||||||
|
|
||||||
function TopLevelItem ({ item, noReply, ...props }) {
|
function TopLevelItem ({ item, noReply, ...props }) {
|
||||||
const ItemComponent = item.isJob ? ItemJob : Item
|
const ItemComponent = item.isJob ? ItemJob : Item
|
||||||
|
const replyRef = useRef()
|
||||||
|
const contentContainerRef = useRef()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ItemComponent
|
<ItemComponent
|
||||||
item={item}
|
item={item}
|
||||||
|
replyRef={replyRef}
|
||||||
full
|
full
|
||||||
right={
|
right={
|
||||||
!noReply &&
|
!noReply &&
|
||||||
|
@ -137,7 +140,7 @@ function TopLevelItem ({ item, noReply, ...props }) {
|
||||||
belowTitle={item.forwards && item.forwards.length > 0 && <FwdUsers forwards={item.forwards} />}
|
belowTitle={item.forwards && item.forwards.length > 0 && <FwdUsers forwards={item.forwards} />}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className={styles.fullItemContainer}>
|
<div className={styles.fullItemContainer} ref={contentContainerRef}>
|
||||||
{item.text && <ItemText item={item} />}
|
{item.text && <ItemText item={item} />}
|
||||||
{item.url && <ItemEmbed item={item} />}
|
{item.url && <ItemEmbed item={item} />}
|
||||||
{item.poll && <Poll item={item} />}
|
{item.poll && <Poll item={item} />}
|
||||||
|
@ -157,7 +160,7 @@ function TopLevelItem ({ item, noReply, ...props }) {
|
||||||
</div>
|
</div>
|
||||||
{!noReply &&
|
{!noReply &&
|
||||||
<>
|
<>
|
||||||
<Reply item={item} replyOpen placeholder={item.ncomments ? undefined : 'start the conversation ...'} />
|
<Reply item={item} replyOpen placeholder={item.ncomments ? undefined : 'start the conversation ...'} ref={replyRef} contentContainerRef={contentContainerRef} />
|
||||||
{!item.position && !item.isJob && !item.parentId && !item.bounty > 0 && <Related title={item.title} itemId={item.id} />}
|
{!item.position && !item.isJob && !item.parentId && !item.bounty > 0 && <Related title={item.title} itemId={item.id} />}
|
||||||
{item.bounty > 0 && <PastBounties item={item} />}
|
{item.bounty > 0 && <PastBounties item={item} />}
|
||||||
</>}
|
</>}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import Link from 'next/link'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import Badge from 'react-bootstrap/Badge'
|
import Badge from 'react-bootstrap/Badge'
|
||||||
|
import Dropdown from 'react-bootstrap/Dropdown'
|
||||||
import Countdown from './countdown'
|
import Countdown from './countdown'
|
||||||
import { abbrNum, numWithUnits } from '../lib/format'
|
import { abbrNum, numWithUnits } from '../lib/format'
|
||||||
import { newComments, commentsViewedAt } from '../lib/new-comments'
|
import { newComments, commentsViewedAt } from '../lib/new-comments'
|
||||||
|
@ -20,7 +21,8 @@ import MuteDropdownItem from './mute'
|
||||||
|
|
||||||
export default function ItemInfo ({
|
export default function ItemInfo ({
|
||||||
item, pendingSats, full, commentsText = 'comments',
|
item, pendingSats, full, commentsText = 'comments',
|
||||||
commentTextSingular = 'comment', className, embellishUser, extraInfo, onEdit, editText
|
commentTextSingular = 'comment', className, embellishUser, extraInfo, onEdit, editText,
|
||||||
|
onQuoteReply
|
||||||
}) {
|
}) {
|
||||||
const editThreshold = new Date(item.createdAt).getTime() + 10 * 60000
|
const editThreshold = new Date(item.createdAt).getTime() + 10 * 60000
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
|
@ -131,6 +133,8 @@ export default function ItemInfo ({
|
||||||
</>}
|
</>}
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
<CopyLinkDropdownItem item={item} />
|
<CopyLinkDropdownItem item={item} />
|
||||||
|
{(item.parentId || item.text) && onQuoteReply &&
|
||||||
|
<Dropdown.Item onClick={onQuoteReply}>quote reply</Dropdown.Item>}
|
||||||
{me && <BookmarkDropdownItem item={item} />}
|
{me && <BookmarkDropdownItem item={item} />}
|
||||||
{me && !item.mine && <SubscribeDropdownItem item={item} />}
|
{me && !item.mine && <SubscribeDropdownItem item={item} />}
|
||||||
{item.otsHash &&
|
{item.otsHash &&
|
||||||
|
|
|
@ -24,7 +24,7 @@ export function SearchTitle ({ title }) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Item ({ item, rank, belowTitle, right, full, children, siblingComments }) {
|
export default function Item ({ item, rank, belowTitle, right, full, children, siblingComments, replyRef }) {
|
||||||
const titleRef = useRef()
|
const titleRef = useRef()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [pendingSats, setPendingSats] = useState(0)
|
const [pendingSats, setPendingSats] = useState(0)
|
||||||
|
@ -85,6 +85,7 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
|
||||||
</div>
|
</div>
|
||||||
<ItemInfo
|
<ItemInfo
|
||||||
full={full} item={item} pendingSats={pendingSats}
|
full={full} item={item} pendingSats={pendingSats}
|
||||||
|
onQuoteReply={replyRef?.current?.quoteReply}
|
||||||
embellishUser={Number(item?.user?.id) === AD_USER_ID && <Badge className={styles.newComment} bg={null}>AD</Badge>}
|
embellishUser={Number(item?.user?.id) === AD_USER_ID && <Badge className={styles.newComment} bg={null}>AD</Badge>}
|
||||||
/>
|
/>
|
||||||
{belowTitle}
|
{belowTitle}
|
||||||
|
|
|
@ -3,12 +3,13 @@ import { gql, useMutation } from '@apollo/client'
|
||||||
import styles from './reply.module.css'
|
import styles from './reply.module.css'
|
||||||
import { COMMENTS } from '../fragments/comments'
|
import { COMMENTS } from '../fragments/comments'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import { useEffect, useState, useRef, useCallback } from 'react'
|
import { forwardRef, useCallback, useEffect, useState, useRef, useImperativeHandle } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import FeeButton from './fee-button'
|
import FeeButton from './fee-button'
|
||||||
import { commentsViewedAfterComment } from '../lib/new-comments'
|
import { commentsViewedAfterComment } from '../lib/new-comments'
|
||||||
import { commentSchema } from '../lib/validate'
|
import { commentSchema } from '../lib/validate'
|
||||||
import Info from './info'
|
import Info from './info'
|
||||||
|
import { quote } from '../lib/md'
|
||||||
|
|
||||||
export function ReplyOnAnotherPage ({ parentId }) {
|
export function ReplyOnAnotherPage ({ parentId }) {
|
||||||
return (
|
return (
|
||||||
|
@ -33,10 +34,36 @@ function FreebieDialog () {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Reply ({ item, onSuccess, replyOpen, children, placeholder }) {
|
export default forwardRef(function Reply ({ item, onSuccess, replyOpen, children, placeholder, contentContainerRef }, ref) {
|
||||||
const [reply, setReply] = useState(replyOpen)
|
const [reply, setReply] = useState(replyOpen)
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
const parentId = item.id
|
const parentId = item.id
|
||||||
|
const replyInput = useRef(null)
|
||||||
|
const formInnerRef = useRef()
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
quoteReply: ({ selectionOnly }) => {
|
||||||
|
if (!reply) {
|
||||||
|
setReply(true)
|
||||||
|
}
|
||||||
|
const selection = window.getSelection()
|
||||||
|
const selectedText = selection.isCollapsed ? undefined : selection.toString()
|
||||||
|
const isSelectedTextInTarget = contentContainerRef?.current?.contains(selection.anchorNode)
|
||||||
|
if ((selection.isCollapsed || !isSelectedTextInTarget) && selectionOnly) return
|
||||||
|
const textToQuote = isSelectedTextInTarget ? selectedText : item.text
|
||||||
|
let updatedValue
|
||||||
|
if (formInnerRef.current && formInnerRef.current.values && !formInnerRef.current.values.text) {
|
||||||
|
updatedValue = quote(textToQuote)
|
||||||
|
} else if (formInnerRef.current?.values?.text) {
|
||||||
|
// append quote reply text if the input already has content
|
||||||
|
updatedValue = `${replyInput.current.value}\n${quote(textToQuote)}`
|
||||||
|
}
|
||||||
|
if (updatedValue) {
|
||||||
|
replyInput.current.value = updatedValue
|
||||||
|
formInnerRef.current.setValues({ text: updatedValue })
|
||||||
|
window.localStorage.setItem(`reply-${parentId}-text`, updatedValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}), [reply, item])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setReply(replyOpen || !!window.localStorage.getItem('reply-' + parentId + '-' + 'text'))
|
setReply(replyOpen || !!window.localStorage.getItem('reply-' + parentId + '-' + 'text'))
|
||||||
|
@ -96,7 +123,6 @@ export default function Reply ({ item, onSuccess, replyOpen, children, placehold
|
||||||
setReply(replyOpen || false)
|
setReply(replyOpen || false)
|
||||||
}, [upsertComment, setReply, parentId])
|
}, [upsertComment, setReply, parentId])
|
||||||
|
|
||||||
const replyInput = useRef(null)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (replyInput.current && reply && !replyOpen) replyInput.current.focus()
|
if (replyInput.current && reply && !replyOpen) replyInput.current.focus()
|
||||||
}, [reply])
|
}, [reply])
|
||||||
|
@ -108,45 +134,51 @@ export default function Reply ({ item, onSuccess, replyOpen, children, placehold
|
||||||
: (
|
: (
|
||||||
<div className={styles.replyButtons}>
|
<div className={styles.replyButtons}>
|
||||||
<div
|
<div
|
||||||
onClick={() => setReply(!reply)}
|
onPointerDown={e => {
|
||||||
|
if (!reply) {
|
||||||
|
e.preventDefault()
|
||||||
|
ref?.current?.quoteReply({ selectionOnly: true })
|
||||||
|
}
|
||||||
|
setReply(!reply)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{reply ? 'cancel' : 'reply'}
|
{reply ? 'cancel' : 'reply'}
|
||||||
</div>
|
</div>
|
||||||
{/* HACK if we need more items, we should probably do a comment toolbar */}
|
{/* HACK if we need more items, we should probably do a comment toolbar */}
|
||||||
{children}
|
{children}
|
||||||
</div>)}
|
</div>)}
|
||||||
{reply &&
|
<div className={styles.reply} style={{ display: reply ? 'block' : 'none' }}>
|
||||||
<div className={styles.reply}>
|
<Form
|
||||||
<Form
|
initial={{
|
||||||
initial={{
|
text: ''
|
||||||
text: ''
|
}}
|
||||||
}}
|
schema={commentSchema}
|
||||||
schema={commentSchema}
|
invoiceable
|
||||||
invoiceable
|
onSubmit={onSubmit}
|
||||||
onSubmit={onSubmit}
|
storageKeyPrefix={`reply-${parentId}`}
|
||||||
storageKeyPrefix={'reply-' + parentId}
|
innerRef={formInnerRef}
|
||||||
>
|
>
|
||||||
<MarkdownInput
|
<MarkdownInput
|
||||||
name='text'
|
name='text'
|
||||||
minRows={6}
|
minRows={6}
|
||||||
autoFocus={!replyOpen}
|
autoFocus={!replyOpen}
|
||||||
required
|
required
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
hint={me?.sats < 1 && <FreebieDialog />}
|
hint={me?.sats < 1 && <FreebieDialog />}
|
||||||
innerRef={replyInput}
|
innerRef={replyInput}
|
||||||
/>
|
/>
|
||||||
{reply &&
|
{reply &&
|
||||||
<div className='mt-1'>
|
<div className='mt-1'>
|
||||||
<FeeButton
|
<FeeButton
|
||||||
baseFee={1} parentId={parentId} text='reply'
|
baseFee={1} parentId={parentId} text='reply'
|
||||||
ChildButton={SubmitButton} variant='secondary' alwaysShow
|
ChildButton={SubmitButton} variant='secondary' alwaysShow
|
||||||
/>
|
/>
|
||||||
</div>}
|
</div>}
|
||||||
</Form>
|
</Form>
|
||||||
</div>}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
export function ReplySkeleton () {
|
export function ReplySkeleton () {
|
||||||
return (
|
return (
|
||||||
|
|
Loading…
Reference in New Issue