stacker.news/components/item.js

207 lines
6.8 KiB
JavaScript
Raw Normal View History

2021-04-14 23:56:29 +00:00
import Link from 'next/link'
2021-04-14 00:57:32 +00:00
import styles from './item.module.css'
2021-04-22 22:14:32 +00:00
import UpVote from './upvote'
2023-12-27 02:27:52 +00:00
import { useRef } from 'react'
import { USER_ID, UNKNOWN_LINK_REL } from '@/lib/constants'
import Pin from '@/svgs/pushpin-fill.svg'
2022-02-03 22:01:42 +00:00
import reactStringReplace from 'react-string-replace'
import PollIcon from '@/svgs/bar-chart-horizontal-fill.svg'
import BountyIcon from '@/svgs/bounty-bag.svg'
2023-01-26 16:11:55 +00:00
import ActionTooltip from './action-tooltip'
import ImageIcon from '@/svgs/image-fill.svg'
import { numWithUnits } from '@/lib/format'
import ItemInfo from './item-info'
import Prism from '@/svgs/prism.svg'
import { commentsViewedAt } from '@/lib/new-comments'
2023-08-06 19:18:40 +00:00
import { useRouter } from 'next/router'
2023-08-16 19:03:37 +00:00
import { Badge } from 'react-bootstrap'
import AdIcon from '@/svgs/advertisement-fill.svg'
import { DownZap } from './dont-link-this'
import { timeLeft } from '@/lib/time'
import classNames from 'classnames'
import removeMd from 'remove-markdown'
function onItemClick (e, router, item) {
const viewedAt = commentsViewedAt(item)
if (viewedAt) {
e.preventDefault()
if (e.ctrlKey || e.metaKey) {
window.open(
`/items/${item.id}`,
'_blank',
'noopener,noreferrer'
)
} else {
router.push(
`/items/${item.id}?commentsViewedAt=${viewedAt}`,
`/items/${item.id}`)
}
}
}
2022-02-03 22:01:42 +00:00
2022-07-21 22:55:05 +00:00
export function SearchTitle ({ title }) {
return reactStringReplace(title, /\*\*\*([^*]+)\*\*\*/g, (match, i) => {
return <mark key={`strong-${match}-${i}`}>{match}</mark>
2022-02-03 22:01:42 +00:00
})
}
2021-04-14 00:57:32 +00:00
export default function Item ({ item, rank, belowTitle, right, full, children, siblingComments, onQuoteReply, pinnable }) {
2021-09-15 23:42:44 +00:00
const titleRef = useRef()
2023-08-06 19:18:40 +00:00
const router = useRouter()
2021-09-15 23:42:44 +00:00
2023-07-13 20:18:04 +00:00
const image = item.url && item.url.startsWith(process.env.NEXT_PUBLIC_IMGPROXY_URL)
2021-04-14 00:57:32 +00:00
return (
2021-04-14 23:56:29 +00:00
<>
2021-04-22 22:14:32 +00:00
{rank
? (
<div className={styles.rank}>
{rank}
</div>)
: <div />}
2023-10-26 17:52:06 +00:00
<div className={`${styles.item} ${siblingComments ? 'pt-3' : ''}`}>
{item.position && (pinnable || !item.subName)
2022-09-21 19:57:36 +00:00
? <Pin width={24} height={24} className={styles.pin} />
: 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} />
: Number(item.user?.id) === USER_ID.ad
2023-08-16 22:53:51 +00:00
? <AdIcon width={24} height={24} className={styles.ad} />
2023-12-27 02:27:52 +00:00
: <UpVote item={item} className={styles.upvote} />}
2021-04-14 23:56:29 +00:00
<div className={styles.hunk}>
2023-05-01 20:58:30 +00:00
<div className={`${styles.main} flex-wrap`}>
2023-08-06 19:18:40 +00:00
<Link
href={`/items/${item.id}`}
onClick={(e) => onItemClick(e, router, item)}
ref={titleRef}
className={`${styles.title} text-reset me-2`}
2023-08-06 19:18:40 +00:00
>
{item.searchTitle ? <SearchTitle title={item.searchTitle} /> : item.title}
{item.pollCost && <PollIndicator item={item} />}
{item.bounty > 0 &&
<span className={styles.icon}>
<ActionTooltip notForm overlayText={`${numWithUnits(item.bounty)} ${item.bountyPaidTo?.length ? ' paid' : ' bounty'}`}>
<BountyIcon className={`${styles.bountyIcon} ${item.bountyPaidTo?.length ? 'fill-success' : 'fill-grey'}`} height={16} width={16} />
</ActionTooltip>
</span>}
2023-09-26 21:44:57 +00:00
{item.forwards?.length > 0 && <span className={styles.icon}><Prism className='fill-grey ms-1' height={14} width={14} /></span>}
2023-07-24 18:35:05 +00:00
{image && <span className={styles.icon}><ImageIcon className='fill-grey ms-2' height={16} width={16} /></span>}
2021-04-14 23:56:29 +00:00
</Link>
2023-07-13 20:18:04 +00:00
{item.url && !image &&
2024-03-05 01:20:14 +00:00
// eslint-disable-next-line
<a
className={styles.link} target='_blank' href={item.url}
rel={item.rel ?? UNKNOWN_LINK_REL}
>
{item.url.replace(/(^https?:|^)\/\//, '')}
</a>}
2021-04-14 23:56:29 +00:00
</div>
2023-08-16 19:03:37 +00:00
<ItemInfo
2023-12-27 02:27:52 +00:00
full={full} item={item}
onQuoteReply={onQuoteReply}
pinnable={pinnable}
extraBadges={Number(item?.user?.id) === USER_ID.ad && <Badge className={styles.newComment} bg={null}>AD</Badge>}
2023-08-16 19:03:37 +00:00
/>
{belowTitle}
2021-04-14 00:57:32 +00:00
</div>
{right}
2021-04-14 00:57:32 +00:00
</div>
2021-04-14 23:56:29 +00:00
{children && (
<div className={styles.children}>
{children}
</div>
)}
</>
2021-04-14 00:57:32 +00:00
)
}
2021-04-22 22:14:32 +00:00
export function ItemSummary ({ item }) {
const router = useRouter()
const link = (
<Link
href={`/items/${item.id}`}
onClick={(e) => onItemClick(e, router, item)}
className={`${item.title && styles.title} ${styles.summaryText} text-reset me-2`}
>
{item.title ?? removeMd(item.text)}
</Link>
)
const info = (
<ItemInfo
item={item}
showUser={false}
showActionDropdown={false}
extraBadges={item.title && Number(item?.user?.id) === USER_ID.ad && <Badge className={styles.newComment} bg={null}>AD</Badge>}
/>
)
return (
<div className={classNames(styles.item, 'mb-0 pb-0')}>
<div className={styles.hunk}>
{item.title
? (
<>
{link}
{info}
</>
)
: (
<>
{info}
{link}
</>
)}
</div>
</div>
)
}
export function ItemSkeleton ({ rank, children, showUpvote = true }) {
2021-04-22 22:14:32 +00:00
return (
<>
2022-01-27 19:18:48 +00:00
{rank
? (
<div className={styles.rank}>
{rank}
</div>)
: <div />}
2021-04-22 22:14:32 +00:00
<div className={`${styles.item} ${styles.skeleton}`}>
{showUpvote && <UpVote className={styles.upvote} />}
2021-04-22 22:14:32 +00:00
<div className={styles.hunk}>
<div className={`${styles.main} flex-wrap flex-md-nowrap`}>
2023-07-24 18:35:05 +00:00
<span className={`${styles.title} clouds text-reset flex-md-fill flex-md-shrink-0 me-2`} />
2021-04-22 22:14:32 +00:00
<span className={`${styles.link} clouds`} />
</div>
<div className={styles.other}>
2021-04-28 22:52:03 +00:00
<span className={`${styles.otherItem} clouds`} />
2021-04-22 22:14:32 +00:00
<span className={`${styles.otherItem} clouds`} />
<span className={`${styles.otherItem} ${styles.otherItemLonger} clouds`} />
<span className={`${styles.otherItem} ${styles.otherItemLonger} clouds`} />
</div>
</div>
</div>
2021-04-27 00:55:48 +00:00
{children && (
<div className={styles.children}>
{children}
</div>
)}
2021-04-22 22:14:32 +00:00
</>
)
}
function PollIndicator ({ item }) {
const hasExpiration = !!item.pollExpiresAt
const timeRemaining = timeLeft(new Date(item.pollExpiresAt))
const isActive = !hasExpiration || !!timeRemaining
return (
<span className={styles.icon} title={isActive ? 'active' : 'results in'}>
<PollIcon
className={`${isActive
? 'fill-success'
: 'fill-grey'
} ms-1`} height={14} width={14}
/>
</span>
)
}