import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' import Badge from 'react-bootstrap/Badge' import Dropdown from 'react-bootstrap/Dropdown' import Countdown from './countdown' import { abbrNum, numWithUnits } from '@/lib/format' import { newComments, commentsViewedAt } from '@/lib/new-comments' import { timeSince } from '@/lib/time' import { DeleteDropdownItem } from './delete' import styles from './item.module.css' import { useMe } from './me' import DontLikeThisDropdownItem, { OutlawDropdownItem } from './dont-link-this' import BookmarkDropdownItem from './bookmark' import SubscribeDropdownItem from './subscribe' import { CopyLinkDropdownItem, CrosspostDropdownItem } from './share' import Hat from './hat' import { USER_ID } from '@/lib/constants' import ActionDropdown from './action-dropdown' import MuteDropdownItem from './mute' import { DropdownItemUpVote } from './upvote' import { useRoot } from './root' import { MuteSubDropdownItem, PinSubDropdownItem } from './territory-header' import UserPopover from './user-popover' import { useQrPayment } from './payment' import { useRetryCreateItem } from './use-item-submit' import { useToast } from './toast' import { useShowModal } from './modal' export default function ItemInfo ({ item, full, commentsText = 'comments', commentTextSingular = 'comment', className, embellishUser, extraInfo, onEdit, editText, onQuoteReply, extraBadges, nested, pinnable, showActionDropdown = true, showUser = true }) { const editThreshold = new Date(item.invoice?.confirmedAt ?? item.createdAt).getTime() + 10 * 60000 const me = useMe() const toaster = useToast() const router = useRouter() const [canEdit, setCanEdit] = useState(item.mine && (Date.now() < editThreshold)) const [hasNewComments, setHasNewComments] = useState(false) const [meTotalSats, setMeTotalSats] = useState(0) const root = useRoot() const retryCreateItem = useRetryCreateItem({ id: item.id }) const sub = item?.sub || root?.sub useEffect(() => { if (!full) { setHasNewComments(newComments(item)) } }, [item]) useEffect(() => { setCanEdit(item.mine && (Date.now() < editThreshold)) }, [item.mine, editThreshold]) useEffect(() => { if (item) setMeTotalSats(item.meSats || item.meAnonSats || 0) }, [item?.meSats, item?.meAnonSats]) // territory founders can pin any post in their territory // and OPs can pin any root reply in their post const isPost = !item.parentId const mySub = (me && sub && Number(me.id) === sub.userId) const myPost = (me && root && Number(me.id) === Number(root.user.id)) const rootReply = item.path.split('.').length === 2 const canPin = (isPost && mySub) || (myPost && rootReply) const EditInfo = () => { const waitForQrPayment = useQrPayment() if (item.deletedAt) return null let Component let onClick if (me && item.invoice?.actionState && item.invoice?.actionState !== 'PAID') { if (item.invoice?.actionState === 'FAILED') { Component = () => retry payment onClick = async () => { try { const { error } = await retryCreateItem({ variables: { invoiceId: parseInt(item.invoice?.id) } }) if (error) throw error } catch (error) { toaster.danger(error.message) } } } else { Component = () => ( pending ) onClick = () => waitForQrPayment({ id: item.invoice?.id }, null, { cancelOnClose: false }).catch(console.error) } } else if (canEdit) { Component = () => ( <> {editText || 'edit'} { setCanEdit(false) }} /> ) onClick = () => onEdit ? onEdit() : router.push(`/items/${item.id}/edit`) } else { return null } return ( <> \ ) } return (
{!(item.position && (pinnable || !item.subName)) && !(!item.parentId && Number(item.user?.id) === USER_ID.ad) && <> {numWithUnits(item.sats)} \ } {item.boost > 0 && <> {abbrNum(item.boost)} boost \ } { const viewedAt = commentsViewedAt(item) if (viewedAt) { e.preventDefault() router.push( `/items/${item.id}?commentsViewedAt=${viewedAt}`, `/items/${item.id}`) } }} title={numWithUnits(item.commentSats)} className='text-reset position-relative' > {numWithUnits(item.ncomments, { abbreviate: false, unitPlural: commentsText, unitSingular: commentTextSingular })} {hasNewComments && {' '} } \ {showUser && @{item.user.name} {embellishUser} } {timeSince(new Date(item.createdAt))} {item.prior && <> \ yesterday } {item.subName && {' '}{item.subName} } {sub?.nsfw && nsfw} {(item.outlawed && !item.mine && {' '}outlawed ) || (item.freebie && !item.position && {' '}freebie )} {(item.apiKey && <>{' '}bot )} {extraBadges} { showActionDropdown && <> {(item.parentId || item.text) && onQuoteReply && quote reply} {me && } {me && } {item.otsHash && opentimestamp } {item?.noteId && ( window.open(`https://njump.me/${item.noteId}`, '_blank', 'noopener,noreferrer,nofollow')}> nostr note )} {item && item.mine && !item.noteId && !item.isJob && !item.parentId && } {me && !item.position && !item.mine && !item.deletedAt && (item.meDontLikeSats > meTotalSats ? : )} {me && sub && !item.mine && !item.outlawed && Number(me.id) === Number(sub.userId) && sub.moderated && <>
} {item.mine && item.invoice?.id && <>
view invoice } {me && !nested && !item.mine && sub && Number(me.id) !== Number(sub.userId) && <>
} {canPin && <>
} {item.mine && !item.position && !item.deletedAt && !item.bio && <>
} {me && !item.mine && <>
}
} {extraInfo}
) } function InfoDropdownItem ({ item }) { const me = useMe() const showModal = useShowModal() const onClick = () => { showModal((onClose) => { return (
id
{item.id}
created at
{item.createdAt}
cost
{item.cost}
sats
{item.sats}
{me && ( <>
sats from me
{item.meSats}
)}
zappers
{item.upvotes}
) }) } return ( details ) }