fix quote reply by removing imperative logic
This commit is contained in:
parent
2bcf3acda6
commit
d211fe93ea
@ -21,6 +21,7 @@ import ItemInfo from './item-info'
|
|||||||
import Badge from 'react-bootstrap/Badge'
|
import Badge from 'react-bootstrap/Badge'
|
||||||
import { RootProvider, useRoot } from './root'
|
import { RootProvider, useRoot } from './root'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
|
import { useQuoteReply } from './use-quote-reply'
|
||||||
|
|
||||||
function Parent ({ item, rootText }) {
|
function Parent ({ item, rootText }) {
|
||||||
const root = useRoot()
|
const root = useRoot()
|
||||||
@ -108,6 +109,7 @@ export default function Comment ({
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const root = useRoot()
|
const root = useRoot()
|
||||||
const [pendingSats, setPendingSats] = useState(0)
|
const [pendingSats, setPendingSats] = useState(0)
|
||||||
|
const { ref: textRef, quote, quoteReply, cancelQuote } = useQuoteReply({ text: item.text })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCollapse(window.localStorage.getItem(`commentCollapse:${item.id}`) || collapse)
|
setCollapse(window.localStorage.getItem(`commentCollapse:${item.id}`) || collapse)
|
||||||
@ -136,8 +138,6 @@ 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
|
||||||
@ -167,7 +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}
|
onQuoteReply={quoteReply}
|
||||||
extraInfo={
|
extraInfo={
|
||||||
<>
|
<>
|
||||||
{includeParent && <Parent item={item} rootText={rootText} />}
|
{includeParent && <Parent item={item} rootText={rootText} />}
|
||||||
@ -210,7 +210,7 @@ export default function Comment ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<div className={styles.text} ref={contentContainerRef}>
|
<div className={styles.text} ref={textRef}>
|
||||||
{item.searchText
|
{item.searchText
|
||||||
? <SearchText text={item.searchText} />
|
? <SearchText text={item.searchText} />
|
||||||
: (
|
: (
|
||||||
@ -227,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} ref={replyRef} contentContainerRef={contentContainerRef}>
|
<Reply depth={depth + 1} item={item} replyOpen={replyOpen} onCancelQuote={cancelQuote} onQuoteReply={quoteReply} quote={quote}>
|
||||||
{root.bounty && !bountyPaid && <PayBounty item={item} />}
|
{root.bounty && !bountyPaid && <PayBounty item={item} />}
|
||||||
</Reply>}
|
</Reply>}
|
||||||
{children}
|
{children}
|
||||||
|
@ -388,7 +388,7 @@ function FormGroup ({ className, label, children }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function InputInner ({
|
function InputInner ({
|
||||||
prepend, append, hint, showValid, onChange, onBlur, overrideValue,
|
prepend, append, hint, showValid, onChange, onBlur, overrideValue, appendValue,
|
||||||
innerRef, noForm, clear, onKeyDown, inputGroupClassName, debounce: debounceTime, maxLength,
|
innerRef, noForm, clear, onKeyDown, inputGroupClassName, debounce: debounceTime, maxLength,
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
@ -441,6 +441,17 @@ function InputInner ({
|
|||||||
}
|
}
|
||||||
}, [overrideValue])
|
}, [overrideValue])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (appendValue) {
|
||||||
|
const updatedValue = meta.value ? `${meta.value}\n${appendValue}` : appendValue
|
||||||
|
helpers.setValue(updatedValue)
|
||||||
|
if (storageKey) {
|
||||||
|
window.localStorage.setItem(storageKey, updatedValue)
|
||||||
|
}
|
||||||
|
innerRef?.current?.focus()
|
||||||
|
}
|
||||||
|
}, [appendValue])
|
||||||
|
|
||||||
const invalid = (!formik || formik.submitCount > 0) && meta.touched && meta.error
|
const invalid = (!formik || formik.submitCount > 0) && meta.touched && meta.error
|
||||||
|
|
||||||
useEffect(debounce(() => {
|
useEffect(debounce(() => {
|
||||||
|
@ -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, useRef, useState } from 'react'
|
import { useEffect, 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'
|
||||||
@ -25,6 +25,7 @@ import Link from 'next/link'
|
|||||||
import { RootProvider } from './root'
|
import { RootProvider } from './root'
|
||||||
import { IMGPROXY_URL_REGEXP } from '../lib/url'
|
import { IMGPROXY_URL_REGEXP } from '../lib/url'
|
||||||
import { numWithUnits } from '../lib/format'
|
import { numWithUnits } from '../lib/format'
|
||||||
|
import { useQuoteReply } from './use-quote-reply'
|
||||||
|
|
||||||
function BioItem ({ item, handleClick }) {
|
function BioItem ({ item, handleClick }) {
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
@ -122,14 +123,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 { ref: textRef, quote, quoteReply, cancelQuote } = useQuoteReply({ text: item.text })
|
||||||
const contentContainerRef = useRef()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ItemComponent
|
<ItemComponent
|
||||||
item={item}
|
item={item}
|
||||||
replyRef={replyRef}
|
|
||||||
full
|
full
|
||||||
|
onQuoteReply={quoteReply}
|
||||||
right={
|
right={
|
||||||
!noReply &&
|
!noReply &&
|
||||||
<>
|
<>
|
||||||
@ -140,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} ref={contentContainerRef}>
|
<div className={styles.fullItemContainer} ref={textRef}>
|
||||||
{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} />}
|
||||||
@ -160,7 +160,7 @@ function TopLevelItem ({ item, noReply, ...props }) {
|
|||||||
</div>
|
</div>
|
||||||
{!noReply &&
|
{!noReply &&
|
||||||
<>
|
<>
|
||||||
<Reply item={item} replyOpen placeholder={item.ncomments ? undefined : 'start the conversation ...'} ref={replyRef} contentContainerRef={contentContainerRef} />
|
<Reply item={item} replyOpen placeholder={item.ncomments ? undefined : 'start the conversation ...'} onCancelQuote={cancelQuote} onQuoteReply={quoteReply} quote={quote} />
|
||||||
{!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} />}
|
||||||
</>}
|
</>}
|
||||||
|
@ -24,7 +24,7 @@ export function SearchTitle ({ title }) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Item ({ item, rank, belowTitle, right, full, children, siblingComments, replyRef }) {
|
export default function Item ({ item, rank, belowTitle, right, full, children, siblingComments, onQuoteReply }) {
|
||||||
const titleRef = useRef()
|
const titleRef = useRef()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [pendingSats, setPendingSats] = useState(0)
|
const [pendingSats, setPendingSats] = useState(0)
|
||||||
@ -94,7 +94,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}
|
onQuoteReply={onQuoteReply}
|
||||||
nofollow={nofollow}
|
nofollow={nofollow}
|
||||||
extraBadges={Number(item?.user?.id) === AD_USER_ID && <Badge className={styles.newComment} bg={null}>AD</Badge>}
|
extraBadges={Number(item?.user?.id) === AD_USER_ID && <Badge className={styles.newComment} bg={null}>AD</Badge>}
|
||||||
/>
|
/>
|
||||||
|
@ -3,12 +3,11 @@ 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 { forwardRef, useCallback, useEffect, useState, useRef, useImperativeHandle } from 'react'
|
import { forwardRef, useCallback, useEffect, useState, useRef } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { FeeButtonProvider, postCommentBaseLineItems, postCommentUseRemoteLineItems } from './fee-button'
|
import { FeeButtonProvider, postCommentBaseLineItems, postCommentUseRemoteLineItems } 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 { quote } from '../lib/md'
|
|
||||||
import { COMMENT_DEPTH_LIMIT } from '../lib/constants'
|
import { COMMENT_DEPTH_LIMIT } from '../lib/constants'
|
||||||
import { useToast } from './toast'
|
import { useToast } from './toast'
|
||||||
import { toastDeleteScheduled } from '../lib/form'
|
import { toastDeleteScheduled } from '../lib/form'
|
||||||
@ -30,68 +29,18 @@ export function ReplyOnAnotherPage ({ item }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default forwardRef(function Reply ({ item, onSuccess, replyOpen, children, placeholder, contentContainerRef }, ref) {
|
export default forwardRef(function Reply ({ item, onSuccess, replyOpen, children, placeholder, onQuoteReply, onCancelQuote, quote }, ref) {
|
||||||
const [reply, setReply] = useState(replyOpen)
|
const [reply, setReply] = useState(replyOpen || quote)
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
const parentId = item.id
|
const parentId = item.id
|
||||||
const replyInput = useRef(null)
|
const replyInput = useRef(null)
|
||||||
const formInnerRef = useRef()
|
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
|
|
||||||
// Start block to handle iOS Safari's weird selection clearing behavior
|
|
||||||
const savedRange = useRef()
|
|
||||||
const savedRangeNode = useRef()
|
|
||||||
const onTouchEnd = useCallback(() => {
|
|
||||||
const selection = document.getSelection()
|
|
||||||
if (!selection || selection.rangeCount === 0 || selection.getRangeAt(0).length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const range = selection.getRangeAt(0)
|
|
||||||
savedRangeNode.current = range.commonAncestorContainer
|
|
||||||
savedRange.current = range.cloneContents()
|
|
||||||
}, [])
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener('touchend', onTouchEnd)
|
if (replyOpen || quote || !!window.localStorage.getItem('reply-' + parentId + '-' + 'text')) {
|
||||||
return () => document.removeEventListener('touchend', onTouchEnd)
|
setReply(true)
|
||||||
}, [])
|
|
||||||
// End block to handle iOS Safari's weird selection clearing behavior
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
quoteReply: ({ selectionOnly }) => {
|
|
||||||
if (!reply) {
|
|
||||||
setReply(true)
|
|
||||||
}
|
|
||||||
const selection = window.getSelection()
|
|
||||||
let selectedText = selection.isCollapsed ? undefined : selection.toString()
|
|
||||||
let isSelectedTextInTarget = contentContainerRef?.current?.contains(selection.anchorNode)
|
|
||||||
|
|
||||||
// Start block to handle iOS Safari's weird selection clearing behavior
|
|
||||||
if (!selectedText && savedRange.current && savedRangeNode.current) {
|
|
||||||
selectedText = savedRange.current.textContent
|
|
||||||
isSelectedTextInTarget = contentContainerRef?.current?.contains(savedRangeNode.current)
|
|
||||||
}
|
|
||||||
// End block to handle iOS Safari's weird selection clearing behavior
|
|
||||||
|
|
||||||
if ((selection.isCollapsed || !isSelectedTextInTarget || !selectedText) && 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])
|
}, [replyOpen, quote, parentId])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setReply(replyOpen || !!window.localStorage.getItem('reply-' + parentId + '-' + 'text'))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const [upsertComment] = useMutation(
|
const [upsertComment] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
@ -160,12 +109,17 @@ export default forwardRef(function Reply ({ item, onSuccess, replyOpen, children
|
|||||||
: (
|
: (
|
||||||
<div className={styles.replyButtons}>
|
<div className={styles.replyButtons}>
|
||||||
<div
|
<div
|
||||||
className='pe-3' onPointerDown={e => {
|
className='pe-3'
|
||||||
if (!reply) {
|
onPointerDown={e => {
|
||||||
|
if (reply) {
|
||||||
|
window.localStorage.removeItem('reply-' + parentId + '-' + 'text')
|
||||||
|
setReply(false)
|
||||||
|
onCancelQuote?.()
|
||||||
|
} else {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
ref?.current?.quoteReply({ selectionOnly: true })
|
onQuoteReply({ selectionOnly: true })
|
||||||
|
setReply(true)
|
||||||
}
|
}
|
||||||
setReply(!reply)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{reply ? 'cancel' : 'reply'}
|
{reply ? 'cancel' : 'reply'}
|
||||||
@ -187,15 +141,14 @@ export default forwardRef(function Reply ({ item, onSuccess, replyOpen, children
|
|||||||
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
|
||||||
|
appendValue={quote}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
innerRef={replyInput}
|
|
||||||
/>
|
/>
|
||||||
<ItemButtonBar createText='reply' hasCancel={false} />
|
<ItemButtonBar createText='reply' hasCancel={false} />
|
||||||
</Form>
|
</Form>
|
||||||
|
24
components/use-quote-reply.js
Normal file
24
components/use-quote-reply.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { useCallback, useRef, useState } from 'react'
|
||||||
|
import { quote as quoteMd } from '../lib/md'
|
||||||
|
|
||||||
|
export function useQuoteReply ({ text }) {
|
||||||
|
const ref = useRef(null)
|
||||||
|
const [quote, setQuote] = useState(null)
|
||||||
|
|
||||||
|
const quoteReply = useCallback(({ selectionOnly }) => {
|
||||||
|
const selection = window.getSelection()
|
||||||
|
const selectedText = selection.isCollapsed ? undefined : selection.toString()
|
||||||
|
const isSelectedTextInTarget = ref?.current?.contains(selection.anchorNode)
|
||||||
|
|
||||||
|
if ((selection.isCollapsed || !isSelectedTextInTarget || !selectedText) && selectionOnly) return
|
||||||
|
|
||||||
|
const textToQuote = isSelectedTextInTarget ? selectedText : text
|
||||||
|
setQuote(quoteMd(textToQuote))
|
||||||
|
}, [ref?.current, text])
|
||||||
|
|
||||||
|
const cancelQuote = useCallback(() => {
|
||||||
|
setQuote(null)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return { ref, quote, quoteReply, cancelQuote }
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user