stacker.news/components/comment.js

278 lines
11 KiB
JavaScript
Raw Normal View History

2021-04-14 23:56:29 +00:00
import itemStyles from './item.module.css'
import styles from './comment.module.css'
import Text, { SearchText } from './text'
2021-04-14 23:56:29 +00:00
import Link from 'next/link'
2022-05-17 22:09:15 +00:00
import Reply, { ReplyOnAnotherPage } from './reply'
import { useEffect, useMemo, useRef, useState } from 'react'
2021-04-22 22:14:32 +00:00
import UpVote from './upvote'
import Eye from '@/svgs/eye-fill.svg'
import EyeClose from '@/svgs/eye-close-line.svg'
2021-06-24 23:56:01 +00:00
import { useRouter } from 'next/router'
2021-08-10 22:59:06 +00:00
import CommentEdit from './comment-edit'
import { USER_ID, COMMENT_DEPTH_LIMIT, UNKNOWN_LINK_REL } from '@/lib/constants'
2023-01-26 16:11:55 +00:00
import PayBounty from './pay-bounty'
import BountyIcon from '@/svgs/bounty-bag.svg'
2023-01-26 16:11:55 +00:00
import ActionTooltip from './action-tooltip'
import { numWithUnits } from '@/lib/format'
2022-12-19 22:27:52 +00:00
import Share from './share'
import ItemInfo from './item-info'
2023-07-24 18:35:05 +00:00
import Badge from 'react-bootstrap/Badge'
2023-05-06 21:51:17 +00:00
import { RootProvider, useRoot } from './root'
import { useMe } from './me'
import { useQuoteReply } from './use-quote-reply'
import { DownZap } from './dont-link-this'
import Skull from '@/svgs/death-skull.svg'
import { commentSubTreeRootId } from '@/lib/item'
import Pin from '@/svgs/pushpin-fill.svg'
import LinkToContext from './link-to-context'
2021-04-14 23:56:29 +00:00
function Parent ({ item, rootText }) {
2023-05-06 21:51:17 +00:00
const root = useRoot()
2021-04-15 19:41:02 +00:00
const ParentFrag = () => (
<>
<span> \ </span>
<Link href={`/items/${item.parentId}`} className='text-reset'>
parent
2021-04-15 19:41:02 +00:00
</Link>
</>
)
return (
<>
2023-05-06 21:51:17 +00:00
{Number(root.id) !== Number(item.parentId) && <ParentFrag />}
2021-04-15 19:41:02 +00:00
<span> \ </span>
<Link href={`/items/${root.id}`} className='text-reset'>
{rootText || 'on:'} {root?.title}
2021-04-15 19:41:02 +00:00
</Link>
2023-05-06 21:51:17 +00:00
{root.subName &&
<Link href={`/~${root.subName}`}>
2023-07-24 18:35:05 +00:00
{' '}<Badge className={itemStyles.newComment} bg={null}>{root.subName}</Badge>
2023-05-02 16:55:10 +00:00
</Link>}
2021-04-15 19:41:02 +00:00
</>
)
}
2021-12-16 20:02:17 +00:00
const truncateString = (string = '', maxLength = 140) =>
string.length > maxLength
? `${string.substring(0, maxLength)} […]`
: string
2023-10-26 17:52:06 +00:00
export function CommentFlat ({ item, rank, siblingComments, ...props }) {
2022-01-27 19:18:48 +00:00
const router = useRouter()
const [href, as] = useMemo(() => {
2024-01-18 01:02:59 +00:00
const rootId = commentSubTreeRootId(item)
return [{
pathname: '/items/[id]',
query: { id: rootId, commentId: item.id }
}, `/items/${rootId}`]
}, [item?.id])
2022-01-27 19:18:48 +00:00
return (
<>
{rank
? (
<div className={`${itemStyles.rank} pt-2 align-self-start`}>
{rank}
</div>)
: <div />}
<LinkToContext
className={siblingComments ? 'py-3' : 'py-2'}
onClick={e => {
router.push(href, as)
}}
href={href}
>
<RootProvider root={item.root}>
<Comment item={item} {...props} />
</RootProvider>
</LinkToContext>
</>
2022-01-27 19:18:48 +00:00
)
}
2021-09-23 17:42:00 +00:00
export default function Comment ({
item, children, replyOpen, includeParent, topLevel,
rootText, noComments, noReply, truncate, depth, pin
2021-09-23 17:42:00 +00:00
}) {
2021-08-10 22:59:06 +00:00
const [edit, setEdit] = useState()
const me = useMe()
2023-11-21 23:26:24 +00:00
const isHiddenFreebie = !me?.privates?.wildWestMode && !me?.privates?.greeterMode && !item.mine && item.freebie && !item.freedFreebie
const [collapse, setCollapse] = useState(
2023-12-28 00:14:22 +00:00
(isHiddenFreebie || item?.user?.meMute || (item?.outlawed && !me?.privates?.wildWestMode)) && !includeParent
? 'yep'
: 'nope')
2021-06-24 23:56:01 +00:00
const ref = useRef(null)
const router = useRouter()
2023-05-06 21:51:17 +00:00
const root = useRoot()
const { ref: textRef, quote, quoteReply, cancelQuote } = useQuoteReply({ text: item.text })
2021-06-24 23:56:01 +00:00
useEffect(() => {
2023-07-25 14:14:45 +00:00
setCollapse(window.localStorage.getItem(`commentCollapse:${item.id}`) || collapse)
2021-06-24 23:56:01 +00:00
if (Number(router.query.commentId) === Number(item.id)) {
// HACK wait for other comments to collapse if they're collapsed
setTimeout(() => {
ref.current.scrollIntoView({ behavior: 'instant', block: 'start' })
ref.current.classList.add('outline-it')
}, 100)
2021-06-24 23:56:01 +00:00
}
2023-08-05 17:13:15 +00:00
}, [item.id, router.query.commentId])
2021-04-14 23:56:29 +00:00
2023-08-06 19:18:40 +00:00
useEffect(() => {
if (router.query.commentsViewedAt &&
me?.id !== item.user?.id &&
new Date(item.createdAt).getTime() > router.query.commentsViewedAt) {
ref.current.classList.add('outline-new-comment')
}
}, [item.id])
2022-05-17 22:09:15 +00:00
const bottomedOut = depth === COMMENT_DEPTH_LIMIT
// Don't show OP badge when anon user comments on anon user posts
const op = root.user.name === item.user.name && Number(item.user.id) !== USER_ID.anon
? 'OP'
: root.forwards?.some(f => f.user.name === item.user.name) && Number(item.user.id) !== USER_ID.anon
2023-09-27 18:20:16 +00:00
? 'fwd'
: null
2023-05-06 21:51:17 +00:00
const bountyPaid = root.bountyPaidTo?.includes(Number(item.id))
2021-10-27 18:26:34 +00:00
2021-04-14 23:56:29 +00:00
return (
2021-11-09 22:43:56 +00:00
<div
ref={ref} className={includeParent ? '' : `${styles.comment} ${collapse === 'yep' ? styles.collapsed : ''}`}
onMouseEnter={() => ref.current.classList.add('outline-new-comment-unset')}
onTouchStart={() => ref.current.classList.add('outline-new-comment-unset')}
2021-06-24 23:56:01 +00:00
>
2021-04-28 19:30:14 +00:00
<div className={`${itemStyles.item} ${styles.item}`}>
2023-12-28 00:14:22 +00:00
{item.outlawed && !me?.privates?.wildWestMode
? <Skull className={styles.dontLike} width={24} height={24} />
: item.meDontLikeSats > item.meSats
Frontend payment UX cleanup (#1194) * Replace useInvoiceable with usePayment hook * Show WebLnError in QR code fallback * Fix missing removal of old zap undo code * Fix payment timeout message * Fix unused arg in super() * Also bail if invoice expired * Fix revert on reply error * Use JIT_INVOICE_TIMEOUT_MS constant * Remove unnecessary PaymentContext * Fix me as a dependency in FeeButtonContext * Fix anon sats added before act success * Optimistic updates for zaps * Fix modal not closed after custom zap * Optimistic update for custom zaps * Optimistic update for bounty payments * Consistent error handling for zaps and bounty payments * Optimistic update for poll votes * Use var balance in payment.request * Rename invoiceable to prepaid * Log cancelled invoices * Client notifications We now show notifications that are stored on the client to inform the user about following errors in the prepaid payment flow: - if a payment fails - if an invoice expires before it is paid - if a payment was interrupted (for example via page refresh) - if the action fails after payment * Remove unnecessary passing of act * Use AbortController for zap undos * Fix anon zap update not updating bolt color * Fix zap counted towards anon sats even if logged in * Fix duplicate onComplete call * Fix downzap type error * Fix "missing field 'path' while writing result" error * Pass full item in downzap props The previous commit fixed cache updates for downzaps but then the cache update for custom zaps failed because 'path' wasn't included in the server response. This commit is the proper fix. * Parse lnc rpc error messages * Add hash to InvoiceExpiredError
2024-05-28 17:18:54 +00:00
? <DownZap width={24} height={24} className={styles.dontLike} item={item} />
: pin ? <Pin width={22} height={22} className={styles.pin} /> : <UpVote item={item} className={styles.upvote} />}
2021-04-30 21:42:51 +00:00
<div className={`${itemStyles.hunk} ${styles.hunk}`}>
<div className='d-flex align-items-center'>
2023-09-28 20:02:25 +00:00
{item.user?.meMute && !includeParent && collapse === 'yep'
? (
<span
className={`${itemStyles.other} ${styles.other} pointer`} onClick={() => {
setCollapse('nope')
window.localStorage.setItem(`commentCollapse:${item.id}`, 'nope')
}}
>reply from someone you muted
</span>)
: <ItemInfo
item={item}
commentsText='replies'
commentTextSingular='reply'
className={`${itemStyles.other} ${styles.other}`}
embellishUser={op && <><span> </span><Badge bg={op === 'fwd' ? 'secondary' : 'boost'} className={`${styles.op} bg-opacity-75`}>{op}</Badge></>}
onQuoteReply={quoteReply}
2023-12-31 16:52:19 +00:00
nested={!includeParent}
2023-09-28 20:02:25 +00:00
extraInfo={
<>
{includeParent && <Parent item={item} rootText={rootText} />}
{bountyPaid &&
<ActionTooltip notForm overlayText={`${numWithUnits(root.bounty)} paid`}>
<BountyIcon className={`${styles.bountyIcon} ${'fill-success vertical-align-middle'}`} height={16} width={16} />
</ActionTooltip>}
</>
}
onEdit={e => { setEdit(!edit) }}
editText={edit ? 'cancel' : 'edit'}
/>}
{!includeParent && (collapse === 'yep'
2021-10-27 18:35:26 +00:00
? <Eye
className={styles.collapser} height={10} width={10} onClick={() => {
setCollapse('nope')
2023-07-25 14:14:45 +00:00
window.localStorage.setItem(`commentCollapse:${item.id}`, 'nope')
2021-10-27 18:35:26 +00:00
}}
/>
: <EyeClose
className={styles.collapser} height={10} width={10} onClick={() => {
setCollapse('yep')
2023-07-25 14:14:45 +00:00
window.localStorage.setItem(`commentCollapse:${item.id}`, 'yep')
2021-10-27 18:35:26 +00:00
}}
/>)}
{topLevel && (
2023-07-24 18:35:05 +00:00
<span className='d-flex ms-auto align-items-center'>
2023-12-15 18:10:29 +00:00
<Share title={item?.title} path={`/items/${item?.id}`} />
</span>
)}
2021-04-14 23:56:29 +00:00
</div>
2021-08-10 22:59:06 +00:00
{edit
? (
2021-09-23 20:09:07 +00:00
<CommentEdit
comment={item}
onSuccess={() => {
setEdit(!edit)
}}
/>
2021-08-10 22:59:06 +00:00
)
: (
<div className={styles.text} ref={textRef}>
{item.searchText
? <SearchText text={item.searchText} />
: (
<Text itemId={item.id} topLevel={topLevel} rel={item.rel ?? UNKNOWN_LINK_REL} outlawed={item.outlawed} imgproxyUrls={item.imgproxyUrls}>
2023-12-28 00:14:22 +00:00
{item.outlawed && !me?.privates?.wildWestMode
? '*stackers have outlawed this. turn on wild west mode in your [settings](/settings) to see outlawed content.*'
: truncate ? truncateString(item.text) : item.text}
</Text>)}
2021-08-10 22:59:06 +00:00
</div>
)}
2021-04-14 23:56:29 +00:00
</div>
</div>
{collapse !== 'yep' && (
bottomedOut
? <div className={styles.children}><ReplyOnAnotherPage item={item} /></div>
: (
2023-07-25 14:14:45 +00:00
<div className={styles.children}>
2023-12-28 00:14:22 +00:00
{item.outlawed && !me?.privates?.wildWestMode
? <div className='py-2' />
: !noReply &&
<Reply depth={depth + 1} item={item} replyOpen={replyOpen} onCancelQuote={cancelQuote} onQuoteReply={quoteReply} quote={quote}>
{root.bounty && !bountyPaid && <PayBounty item={item} />}
</Reply>}
{children}
2023-07-25 18:32:48 +00:00
<div className={styles.comments}>
{item.comments && !noComments
? item.comments.map((item) => (
<Comment depth={depth + 1} key={item.id} item={item} />
2023-07-25 14:14:45 +00:00
))
: null}
</div>
2022-05-17 22:09:15 +00:00
</div>
)
)}
2022-05-17 22:09:15 +00:00
</div>
)
}
2021-04-22 22:14:32 +00:00
export function CommentSkeleton ({ skeletonChildren }) {
return (
2021-04-28 22:52:03 +00:00
<div className={styles.comment}>
2021-04-22 22:14:32 +00:00
<div className={`${itemStyles.item} ${itemStyles.skeleton} ${styles.item} ${styles.skeleton}`}>
<UpVote className={styles.upvote} />
<div className={`${itemStyles.hunk} ${styles.hunk}`}>
<div className={itemStyles.other}>
2021-04-28 22:52:03 +00:00
<span className={`${itemStyles.otherItem} clouds`} />
<span className={`${itemStyles.otherItem} clouds`} />
2021-04-22 22:14:32 +00:00
<span className={`${itemStyles.otherItem} clouds`} />
<span className={`${itemStyles.otherItem} ${itemStyles.otherItemLonger} clouds`} />
</div>
<div className={`${styles.text} clouds`} />
2021-04-14 23:56:29 +00:00
</div>
2021-04-22 22:14:32 +00:00
</div>
2021-04-28 22:52:03 +00:00
<div className={`${itemStyles.children} ${styles.children} ${styles.skeleton}`}>
<div className={styles.replyPadder}>
<div className={`${itemStyles.other} ${styles.reply} clouds`} />
</div>
2023-07-24 18:35:05 +00:00
<div className={`${styles.comments} ms-sm-1 ms-md-3`}>
2021-05-05 18:13:14 +00:00
{skeletonChildren
? <CommentSkeleton skeletonChildren={skeletonChildren - 1} />
2021-04-17 18:15:18 +00:00
: null}
</div>
2021-04-14 23:56:29 +00:00
</div>
2021-04-28 22:52:03 +00:00
</div>
2021-04-14 23:56:29 +00:00
)
}