diff --git a/components/comment.js b/components/comment.js index 9955efe6..f370d114 100644 --- a/components/comment.js +++ b/components/comment.js @@ -21,6 +21,7 @@ import ItemInfo from './item-info' import Badge from 'react-bootstrap/Badge' import { RootProvider, useRoot } from './root' import { useMe } from './me' +import { useQuoteReply } from './use-quote-reply' function Parent ({ item, rootText }) { const root = useRoot() @@ -108,6 +109,7 @@ export default function Comment ({ const router = useRouter() const root = useRoot() const [pendingSats, setPendingSats] = useState(0) + const { ref: textRef, quote, quoteReply, cancelQuote } = useQuoteReply({ text: item.text }) useEffect(() => { setCollapse(window.localStorage.getItem(`commentCollapse:${item.id}`) || collapse) @@ -136,8 +138,6 @@ export default function Comment ({ ? 'fwd' : null const bountyPaid = root.bountyPaidTo?.includes(Number(item.id)) - const replyRef = useRef() - const contentContainerRef = useRef() return (
{op}} - onQuoteReply={replyRef?.current?.quoteReply} + onQuoteReply={quoteReply} extraInfo={ <> {includeParent && } @@ -210,7 +210,7 @@ export default function Comment ({ /> ) : ( -
+
{item.searchText ? : ( @@ -227,7 +227,7 @@ export default function Comment ({ : (
{!noReply && - + {root.bounty && !bountyPaid && } } {children} diff --git a/components/form.js b/components/form.js index 8529e552..874521b1 100644 --- a/components/form.js +++ b/components/form.js @@ -388,7 +388,7 @@ function FormGroup ({ className, label, children }) { } 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, ...props }) { @@ -441,6 +441,17 @@ function InputInner ({ } }, [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 useEffect(debounce(() => { diff --git a/components/item-full.js b/components/item-full.js index 6c05f317..d554a8b8 100644 --- a/components/item-full.js +++ b/components/item-full.js @@ -13,7 +13,7 @@ import Button from 'react-bootstrap/Button' import { TwitterTweetEmbed } from 'react-twitter-embed' import YouTube from 'react-youtube' import useDarkMode from './dark-mode' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useState } from 'react' import Poll from './poll' import { commentsViewed } from '../lib/new-comments' import Related from './related' @@ -25,6 +25,7 @@ import Link from 'next/link' import { RootProvider } from './root' import { IMGPROXY_URL_REGEXP } from '../lib/url' import { numWithUnits } from '../lib/format' +import { useQuoteReply } from './use-quote-reply' function BioItem ({ item, handleClick }) { const me = useMe() @@ -122,14 +123,13 @@ function FwdUsers ({ forwards }) { function TopLevelItem ({ item, noReply, ...props }) { const ItemComponent = item.isJob ? ItemJob : Item - const replyRef = useRef() - const contentContainerRef = useRef() + const { ref: textRef, quote, quoteReply, cancelQuote } = useQuoteReply({ text: item.text }) return ( @@ -140,7 +140,7 @@ function TopLevelItem ({ item, noReply, ...props }) { belowTitle={item.forwards && item.forwards.length > 0 && } {...props} > -
+
{item.text && } {item.url && } {item.poll && } @@ -160,7 +160,7 @@ function TopLevelItem ({ item, noReply, ...props }) {
{!noReply && <> - + {!item.position && !item.isJob && !item.parentId && !item.bounty > 0 && } {item.bounty > 0 && } } diff --git a/components/item.js b/components/item.js index 49e0a8ad..ab0ce3c1 100644 --- a/components/item.js +++ b/components/item.js @@ -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 router = useRouter() const [pendingSats, setPendingSats] = useState(0) @@ -94,7 +94,7 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
AD} /> diff --git a/components/reply.js b/components/reply.js index 97cef419..122e0dd6 100644 --- a/components/reply.js +++ b/components/reply.js @@ -3,12 +3,11 @@ import { gql, useMutation } from '@apollo/client' import styles from './reply.module.css' import { COMMENTS } from '../fragments/comments' 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 { FeeButtonProvider, postCommentBaseLineItems, postCommentUseRemoteLineItems } from './fee-button' import { commentsViewedAfterComment } from '../lib/new-comments' import { commentSchema } from '../lib/validate' -import { quote } from '../lib/md' import { COMMENT_DEPTH_LIMIT } from '../lib/constants' import { useToast } from './toast' 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) { - const [reply, setReply] = useState(replyOpen) +export default forwardRef(function Reply ({ item, onSuccess, replyOpen, children, placeholder, onQuoteReply, onCancelQuote, quote }, ref) { + const [reply, setReply] = useState(replyOpen || quote) const me = useMe() const parentId = item.id const replyInput = useRef(null) - const formInnerRef = useRef() 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(() => { - document.addEventListener('touchend', onTouchEnd) - return () => document.removeEventListener('touchend', onTouchEnd) - }, []) - // 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) - } + if (replyOpen || quote || !!window.localStorage.getItem('reply-' + parentId + '-' + 'text')) { + setReply(true) } - }), [reply, item]) - - useEffect(() => { - setReply(replyOpen || !!window.localStorage.getItem('reply-' + parentId + '-' + 'text')) - }, []) + }, [replyOpen, quote, parentId]) const [upsertComment] = useMutation( gql` @@ -160,12 +109,17 @@ export default forwardRef(function Reply ({ item, onSuccess, replyOpen, children : (
{ - if (!reply) { + className='pe-3' + onPointerDown={e => { + if (reply) { + window.localStorage.removeItem('reply-' + parentId + '-' + 'text') + setReply(false) + onCancelQuote?.() + } else { e.preventDefault() - ref?.current?.quoteReply({ selectionOnly: true }) + onQuoteReply({ selectionOnly: true }) + setReply(true) } - setReply(!reply) }} > {reply ? 'cancel' : 'reply'} @@ -187,15 +141,14 @@ export default forwardRef(function Reply ({ item, onSuccess, replyOpen, children invoiceable onSubmit={onSubmit} storageKeyPrefix={`reply-${parentId}`} - innerRef={formInnerRef} > diff --git a/components/use-quote-reply.js b/components/use-quote-reply.js new file mode 100644 index 00000000..aed0b214 --- /dev/null +++ b/components/use-quote-reply.js @@ -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 } +}