Merge branch 'master' into 266-zaps-without-account
This commit is contained in:
commit
118f591d04
|
@ -10,7 +10,7 @@ import {
|
||||||
DONT_LIKE_THIS_COST, COMMENT_DEPTH_LIMIT, COMMENT_TYPE_QUERY,
|
DONT_LIKE_THIS_COST, COMMENT_DEPTH_LIMIT, COMMENT_TYPE_QUERY,
|
||||||
ANON_COMMENT_FEE, ANON_USER_ID, ANON_POST_FEE
|
ANON_COMMENT_FEE, ANON_USER_ID, ANON_POST_FEE
|
||||||
} from '../../lib/constants'
|
} from '../../lib/constants'
|
||||||
import { msatsToSats } from '../../lib/format'
|
import { msatsToSats, numWithUnits } from '../../lib/format'
|
||||||
import { parse } from 'tldts'
|
import { parse } from 'tldts'
|
||||||
import uu from 'url-unshort'
|
import uu from 'url-unshort'
|
||||||
import { amountSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '../../lib/validate'
|
import { amountSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '../../lib/validate'
|
||||||
|
@ -755,7 +755,7 @@ export default {
|
||||||
const [{ item_act: vote }] = await serialize(models, ...calls)
|
const [{ item_act: vote }] = await serialize(models, ...calls)
|
||||||
|
|
||||||
const updatedItem = await models.item.findUnique({ where: { id: Number(id) } })
|
const updatedItem = await models.item.findUnique({ where: { id: Number(id) } })
|
||||||
const title = `your ${updatedItem.title ? 'post' : 'reply'} ${updatedItem.fwdUser ? 'forwarded' : 'stacked'} ${Math.floor(Number(updatedItem.msats) / 1000)} sats${updatedItem.fwdUser ? ` to @${updatedItem.fwdUser.name}` : ''}`
|
const title = `your ${updatedItem.title ? 'post' : 'reply'} ${updatedItem.fwdUser ? 'forwarded' : 'stacked'} ${numWithUnits(msatsToSats(updatedItem.msats))}${updatedItem.fwdUser ? ` to @${updatedItem.fwdUser.name}` : ''}`
|
||||||
sendUserNotification(updatedItem.userId, {
|
sendUserNotification(updatedItem.userId, {
|
||||||
title,
|
title,
|
||||||
body: updatedItem.title ? updatedItem.title : updatedItem.text,
|
body: updatedItem.title ? updatedItem.title : updatedItem.text,
|
||||||
|
|
|
@ -32,6 +32,11 @@ export async function getInvoice (parent, { id }, { me, models }) {
|
||||||
throw new GraphQLError('not ur invoice', { extensions: { code: 'FORBIDDEN' } })
|
throw new GraphQLError('not ur invoice', { extensions: { code: 'FORBIDDEN' } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
inv.nostr = JSON.parse(inv.desc)
|
||||||
|
} catch (err) {
|
||||||
|
}
|
||||||
|
|
||||||
return inv
|
return inv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ export default gql`
|
||||||
cancelled: Boolean!
|
cancelled: Boolean!
|
||||||
confirmedAt: Date
|
confirmedAt: Date
|
||||||
satsReceived: Int
|
satsReceived: Int
|
||||||
|
nostr: JSONObject
|
||||||
}
|
}
|
||||||
|
|
||||||
type Withdrawl {
|
type Withdrawl {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { Input, InputUserSuggest } from './form'
|
||||||
import InputGroup from 'react-bootstrap/InputGroup'
|
import InputGroup from 'react-bootstrap/InputGroup'
|
||||||
import { BOOST_MIN } from '../lib/constants'
|
import { BOOST_MIN } from '../lib/constants'
|
||||||
import Info from './info'
|
import Info from './info'
|
||||||
|
import { numWithUnits } from '../lib/format'
|
||||||
|
|
||||||
export function AdvPostInitial ({ forward }) {
|
export function AdvPostInitial ({ forward }) {
|
||||||
return {
|
return {
|
||||||
|
@ -23,10 +24,10 @@ export default function AdvPostForm ({ edit }) {
|
||||||
<Info>
|
<Info>
|
||||||
<ol className='fw-bold'>
|
<ol className='fw-bold'>
|
||||||
<li>Boost ranks posts higher temporarily based on the amount</li>
|
<li>Boost ranks posts higher temporarily based on the amount</li>
|
||||||
<li>The minimum boost is {BOOST_MIN} sats</li>
|
<li>The minimum boost is {numWithUnits(BOOST_MIN, { abbreviate: false })}</li>
|
||||||
<li>Each {BOOST_MIN} sats of boost is equivalent to one trusted upvote
|
<li>Each {numWithUnits(BOOST_MIN, { abbreviate: false })} of boost is equivalent to one trusted upvote
|
||||||
<ul>
|
<ul>
|
||||||
<li>e.g. {BOOST_MIN * 2} sats is like 2 votes</li>
|
<li>e.g. {numWithUnits(BOOST_MIN * 2, { abbreviate: false })} is like 2 votes</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>The decay of boost "votes" increases at 2x the rate of organic votes
|
<li>The decay of boost "votes" increases at 2x the rate of organic votes
|
||||||
|
|
|
@ -15,7 +15,7 @@ import PayBounty from './pay-bounty'
|
||||||
import BountyIcon from '../svgs/bounty-bag.svg'
|
import BountyIcon from '../svgs/bounty-bag.svg'
|
||||||
import ActionTooltip from './action-tooltip'
|
import ActionTooltip from './action-tooltip'
|
||||||
import Flag from '../svgs/flag-fill.svg'
|
import Flag from '../svgs/flag-fill.svg'
|
||||||
import { abbrNum } from '../lib/format'
|
import { numWithUnits } from '../lib/format'
|
||||||
import Share from './share'
|
import Share from './share'
|
||||||
import ItemInfo from './item-info'
|
import ItemInfo from './item-info'
|
||||||
import Badge from 'react-bootstrap/Badge'
|
import Badge from 'react-bootstrap/Badge'
|
||||||
|
@ -148,13 +148,14 @@ export default function Comment ({
|
||||||
item={item}
|
item={item}
|
||||||
pendingSats={pendingSats}
|
pendingSats={pendingSats}
|
||||||
commentsText='replies'
|
commentsText='replies'
|
||||||
|
commentTextSingular='reply'
|
||||||
className={`${itemStyles.other} ${styles.other}`}
|
className={`${itemStyles.other} ${styles.other}`}
|
||||||
embellishUser={op && <Badge bg='boost' className={`ms-1 ${styles.op} bg-opacity-75`}>OP</Badge>}
|
embellishUser={op && <Badge bg='boost' className={`ms-1 ${styles.op} bg-opacity-75`}>OP</Badge>}
|
||||||
extraInfo={
|
extraInfo={
|
||||||
<>
|
<>
|
||||||
{includeParent && <Parent item={item} rootText={rootText} />}
|
{includeParent && <Parent item={item} rootText={rootText} />}
|
||||||
{bountyPaid &&
|
{bountyPaid &&
|
||||||
<ActionTooltip notForm overlayText={`${abbrNum(root.bounty)} sats paid`}>
|
<ActionTooltip notForm overlayText={`${numWithUnits(root.bounty)} paid`}>
|
||||||
<BountyIcon className={`${styles.bountyIcon} ${'fill-success vertical-align-middle'}`} height={16} width={16} />
|
<BountyIcon className={`${styles.bountyIcon} ${'fill-success vertical-align-middle'}`} height={16} width={16} />
|
||||||
</ActionTooltip>}
|
</ActionTooltip>}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import Comment, { CommentSkeleton } from './comment'
|
||||||
import styles from './header.module.css'
|
import styles from './header.module.css'
|
||||||
import Nav from 'react-bootstrap/Nav'
|
import Nav from 'react-bootstrap/Nav'
|
||||||
import Navbar from 'react-bootstrap/Navbar'
|
import Navbar from 'react-bootstrap/Navbar'
|
||||||
import { abbrNum } from '../lib/format'
|
import { numWithUnits } from '../lib/format'
|
||||||
import { defaultCommentSort } from '../lib/item'
|
import { defaultCommentSort } from '../lib/item'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ export function CommentsHeader ({ handleSort, pinned, bio, parentCreatedAt, comm
|
||||||
activeKey={sort}
|
activeKey={sort}
|
||||||
>
|
>
|
||||||
<Nav.Item className='text-muted'>
|
<Nav.Item className='text-muted'>
|
||||||
{abbrNum(commentSats)} sats
|
{numWithUnits(commentSats)}
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<div className='ms-auto d-flex'>
|
<div className='ms-auto d-flex'>
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import Badge from 'react-bootstrap/Badge'
|
||||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger'
|
import OverlayTrigger from 'react-bootstrap/OverlayTrigger'
|
||||||
import Tooltip from 'react-bootstrap/Tooltip'
|
import Tooltip from 'react-bootstrap/Tooltip'
|
||||||
import CowboyHatIcon from '../svgs/cowboy.svg'
|
import CowboyHatIcon from '../svgs/cowboy.svg'
|
||||||
|
import { numWithUnits } from '../lib/format'
|
||||||
|
|
||||||
export default function CowboyHat ({ user, badge, className = 'ms-1', height = 16, width = 16 }) {
|
export default function CowboyHat ({ user, badge, className = 'ms-1', height = 16, width = 16 }) {
|
||||||
if (user?.streak === null || user.hideCowboyHat) {
|
if (user?.streak === null || user.hideCowboyHat) {
|
||||||
|
@ -10,7 +11,10 @@ export default function CowboyHat ({ user, badge, className = 'ms-1', height = 1
|
||||||
|
|
||||||
const streak = user.streak
|
const streak = user.streak
|
||||||
return (
|
return (
|
||||||
<HatTooltip overlayText={streak ? `${streak} days` : 'new'}>
|
<HatTooltip overlayText={streak
|
||||||
|
? `${numWithUnits(streak, { abbreviate: false, unitSingular: 'day', unitPlural: 'days' })}`
|
||||||
|
: 'new'}
|
||||||
|
>
|
||||||
{badge
|
{badge
|
||||||
? (
|
? (
|
||||||
<Badge bg='grey-medium' className='ms-2 d-inline-flex align-items-center'>
|
<Badge bg='grey-medium' className='ms-2 d-inline-flex align-items-center'>
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
|
import { useEffect } from 'react'
|
||||||
import Table from 'react-bootstrap/Table'
|
import Table from 'react-bootstrap/Table'
|
||||||
import ActionTooltip from './action-tooltip'
|
import ActionTooltip from './action-tooltip'
|
||||||
import Info from './info'
|
import Info from './info'
|
||||||
import styles from './fee-button.module.css'
|
import styles from './fee-button.module.css'
|
||||||
import { gql, useQuery } from '@apollo/client'
|
import { gql, useQuery } from '@apollo/client'
|
||||||
import { useFormikContext } from 'formik'
|
import { useFormikContext } from 'formik'
|
||||||
import { useMe } from './me'
|
|
||||||
import { SSR, ANON_COMMENT_FEE, ANON_POST_FEE } from '../lib/constants'
|
import { SSR, ANON_COMMENT_FEE, ANON_POST_FEE } from '../lib/constants'
|
||||||
import { useEffect } from 'react'
|
import { numWithUnits } from '../lib/format'
|
||||||
|
import { useMe } from './me'
|
||||||
|
|
||||||
function Receipt ({ cost, repetition, hasImgLink, baseFee, parentId, boost }) {
|
function Receipt ({ cost, repetition, hasImgLink, baseFee, parentId, boost }) {
|
||||||
return (
|
return (
|
||||||
<Table className={styles.receipt} borderless size='sm'>
|
<Table className={styles.receipt} borderless size='sm'>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{baseFee} sats</td>
|
<td>{numWithUnits(baseFee, { abbreviate: false })}</td>
|
||||||
<td align='right' className='font-weight-light'>{parentId ? 'reply' : 'post'} fee</td>
|
<td align='right' className='font-weight-light'>{parentId ? 'reply' : 'post'} fee</td>
|
||||||
</tr>
|
</tr>
|
||||||
{hasImgLink &&
|
{hasImgLink &&
|
||||||
|
@ -28,13 +29,13 @@ function Receipt ({ cost, repetition, hasImgLink, baseFee, parentId, boost }) {
|
||||||
</tr>}
|
</tr>}
|
||||||
{boost > 0 &&
|
{boost > 0 &&
|
||||||
<tr>
|
<tr>
|
||||||
<td>+ {boost} sats</td>
|
<td>+ {numWithUnits(boost, { abbreviate: false })}</td>
|
||||||
<td className='font-weight-light' align='right'>boost</td>
|
<td className='font-weight-light' align='right'>boost</td>
|
||||||
</tr>}
|
</tr>}
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<tr>
|
||||||
<td className='fw-bold'>{cost} sats</td>
|
<td className='fw-bold'>{numWithUnits(cost, { abbreviate: false })}</td>
|
||||||
<td align='right' className='font-weight-light'>total fee</td>
|
<td align='right' className='font-weight-light'>total fee</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
|
@ -61,8 +62,8 @@ export default function FeeButton ({ parentId, hasImgLink, baseFee, ChildButton,
|
||||||
const show = alwaysShow || !formik?.isSubmitting
|
const show = alwaysShow || !formik?.isSubmitting
|
||||||
return (
|
return (
|
||||||
<div className='d-flex align-items-center'>
|
<div className='d-flex align-items-center'>
|
||||||
<ActionTooltip overlayText={`${cost} sats`}>
|
<ActionTooltip overlayText={numWithUnits(cost, { abbreviate: false })}>
|
||||||
<ChildButton variant={variant} disabled={disabled}>{text}{cost > baseFee && show && <small> {cost} sats</small>}</ChildButton>
|
<ChildButton variant={variant} disabled={disabled}>{text}{cost > baseFee && show && <small> {numWithUnits(cost, { abbreviate: false })}</small>}</ChildButton>
|
||||||
</ActionTooltip>
|
</ActionTooltip>
|
||||||
{cost > baseFee && show &&
|
{cost > baseFee && show &&
|
||||||
<Info>
|
<Info>
|
||||||
|
@ -79,7 +80,7 @@ function EditReceipt ({ cost, paidSats, addImgLink, boost, parentId }) {
|
||||||
{addImgLink &&
|
{addImgLink &&
|
||||||
<>
|
<>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{paidSats} sats</td>
|
<td>{numWithUnits(paidSats, { abbreviate: false })}</td>
|
||||||
<td align='right' className='font-weight-light'>{parentId ? 'reply' : 'post'} fee</td>
|
<td align='right' className='font-weight-light'>{parentId ? 'reply' : 'post'} fee</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -87,19 +88,19 @@ function EditReceipt ({ cost, paidSats, addImgLink, boost, parentId }) {
|
||||||
<td align='right' className='font-weight-light'>image/link fee</td>
|
<td align='right' className='font-weight-light'>image/link fee</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>- {paidSats} sats</td>
|
<td>- {numWithUnits(paidSats, { abbreviate: false })}</td>
|
||||||
<td align='right' className='font-weight-light'>already paid</td>
|
<td align='right' className='font-weight-light'>already paid</td>
|
||||||
</tr>
|
</tr>
|
||||||
</>}
|
</>}
|
||||||
{boost > 0 &&
|
{boost > 0 &&
|
||||||
<tr>
|
<tr>
|
||||||
<td>+ {boost} sats</td>
|
<td>+ {numWithUnits(boost, { abbreviate: false })}</td>
|
||||||
<td className='font-weight-light' align='right'>boost</td>
|
<td className='font-weight-light' align='right'>boost</td>
|
||||||
</tr>}
|
</tr>}
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<tr>
|
||||||
<td className='fw-bold'>{cost} sats</td>
|
<td className='fw-bold'>{numWithUnits(cost)}</td>
|
||||||
<td align='right' className='font-weight-light'>total fee</td>
|
<td align='right' className='font-weight-light'>total fee</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
|
@ -116,8 +117,8 @@ export function EditFeeButton ({ paidSats, hadImgLink, hasImgLink, ChildButton,
|
||||||
const show = alwaysShow || !formik?.isSubmitting
|
const show = alwaysShow || !formik?.isSubmitting
|
||||||
return (
|
return (
|
||||||
<div className='d-flex align-items-center'>
|
<div className='d-flex align-items-center'>
|
||||||
<ActionTooltip overlayText={`${cost} sats`}>
|
<ActionTooltip overlayText={numWithUnits(cost, { abbreviate: false })}>
|
||||||
<ChildButton variant={variant}>{text}{cost > 0 && show && <small> {cost} sats</small>}</ChildButton>
|
<ChildButton variant={variant}>{text}{cost > 0 && show && <small> {numWithUnits(cost, { abbreviate: false })}</small>}</ChildButton>
|
||||||
</ActionTooltip>
|
</ActionTooltip>
|
||||||
{cost > 0 && show &&
|
{cost > 0 && show &&
|
||||||
<Info>
|
<Info>
|
||||||
|
|
|
@ -1,19 +1,45 @@
|
||||||
|
import AccordianItem from './accordian-item'
|
||||||
import Qr from './qr'
|
import Qr from './qr'
|
||||||
|
import { numWithUnits } from '../lib/format'
|
||||||
|
|
||||||
export function Invoice ({ invoice, onConfirmation, successVerb }) {
|
export function Invoice ({ invoice, onConfirmation, successVerb }) {
|
||||||
let variant = 'default'
|
let variant = 'default'
|
||||||
let status = 'waiting for you'
|
let status = 'waiting for you'
|
||||||
|
let webLn = true
|
||||||
if (invoice.confirmedAt) {
|
if (invoice.confirmedAt) {
|
||||||
variant = 'confirmed'
|
variant = 'confirmed'
|
||||||
status = `${invoice.satsReceived} sats ${successVerb || 'deposited'}`
|
status = `${numWithUnits(invoice.satsReceived, { abbreviate: false })} ${successVerb || 'deposited'}`
|
||||||
onConfirmation?.(invoice)
|
onConfirmation?.(invoice)
|
||||||
|
webLn = false
|
||||||
} else if (invoice.cancelled) {
|
} else if (invoice.cancelled) {
|
||||||
variant = 'failed'
|
variant = 'failed'
|
||||||
status = 'cancelled'
|
status = 'cancelled'
|
||||||
|
webLn = false
|
||||||
} else if (invoice.expiresAt <= new Date()) {
|
} else if (invoice.expiresAt <= new Date()) {
|
||||||
variant = 'failed'
|
variant = 'failed'
|
||||||
status = 'expired'
|
status = 'expired'
|
||||||
|
webLn = false
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Qr webLn value={invoice.bolt11} statusVariant={variant} status={status} />
|
const { nostr } = invoice
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Qr webLn={webLn} value={invoice.bolt11} statusVariant={variant} status={status} />
|
||||||
|
<div className='w-100'>
|
||||||
|
{nostr
|
||||||
|
? <AccordianItem
|
||||||
|
header='Nostr Zap Request'
|
||||||
|
body={
|
||||||
|
<pre>
|
||||||
|
<code>
|
||||||
|
{JSON.stringify(nostr, null, 2)}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import Toc from './table-of-contents'
|
||||||
import Link from 'next/link'
|
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'
|
||||||
|
|
||||||
function BioItem ({ item, handleClick }) {
|
function BioItem ({ item, handleClick }) {
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
|
@ -139,11 +140,11 @@ function TopLevelItem ({ item, noReply, ...props }) {
|
||||||
{item.bountyPaidTo?.length
|
{item.bountyPaidTo?.length
|
||||||
? (
|
? (
|
||||||
<div className='px-3 py-1 d-inline-block bg-grey-medium rounded text-success'>
|
<div className='px-3 py-1 d-inline-block bg-grey-medium rounded text-success'>
|
||||||
<Check className='fill-success' /> {item.bounty} sats paid
|
<Check className='fill-success' /> {numWithUnits(item.bounty, { abbreviate: false })} paid
|
||||||
</div>)
|
</div>)
|
||||||
: (
|
: (
|
||||||
<div className='px-3 py-1 d-inline-block bg-grey-darkmode rounded text-light'>
|
<div className='px-3 py-1 d-inline-block bg-grey-darkmode rounded text-light'>
|
||||||
{item.bounty} sats bounty
|
{numWithUnits(item.bounty, { abbreviate: false })} bounty
|
||||||
</div>)}
|
</div>)}
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'
|
||||||
import Badge from 'react-bootstrap/Badge'
|
import Badge from 'react-bootstrap/Badge'
|
||||||
import Dropdown from 'react-bootstrap/Dropdown'
|
import Dropdown from 'react-bootstrap/Dropdown'
|
||||||
import Countdown from './countdown'
|
import Countdown from './countdown'
|
||||||
import { abbrNum } from '../lib/format'
|
import { abbrNum, numWithUnits } from '../lib/format'
|
||||||
import { newComments, commentsViewedAt } from '../lib/new-comments'
|
import { newComments, commentsViewedAt } from '../lib/new-comments'
|
||||||
import { timeSince } from '../lib/time'
|
import { timeSince } from '../lib/time'
|
||||||
import CowboyHat from './cowboy-hat'
|
import CowboyHat from './cowboy-hat'
|
||||||
|
@ -17,7 +17,10 @@ import BookmarkDropdownItem from './bookmark'
|
||||||
import SubscribeDropdownItem from './subscribe'
|
import SubscribeDropdownItem from './subscribe'
|
||||||
import { CopyLinkDropdownItem } from './share'
|
import { CopyLinkDropdownItem } from './share'
|
||||||
|
|
||||||
export default function ItemInfo ({ item, pendingSats, full, commentsText, className, embellishUser, extraInfo, onEdit, editText }) {
|
export default function ItemInfo ({
|
||||||
|
item, pendingSats, full, commentsText = 'comments',
|
||||||
|
commentTextSingular = 'comment', className, embellishUser, extraInfo, onEdit, editText
|
||||||
|
}) {
|
||||||
const editThreshold = new Date(item.createdAt).getTime() + 10 * 60000
|
const editThreshold = new Date(item.createdAt).getTime() + 10 * 60000
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -40,7 +43,16 @@ export default function ItemInfo ({ item, pendingSats, full, commentsText, class
|
||||||
<div className={className || `${styles.other}`}>
|
<div className={className || `${styles.other}`}>
|
||||||
{!item.position &&
|
{!item.position &&
|
||||||
<>
|
<>
|
||||||
<span title={`from ${item.upvotes} stackers ${item.mine ? `\\ ${item.meSats} sats to post` : `(${meTotalSats} sats from me)`} `}>{abbrNum(item.sats + pendingSats)} sats</span>
|
<span title={`from ${numWithUnits(item.upvotes, {
|
||||||
|
abbreviate: false,
|
||||||
|
unitSingular: 'stacker',
|
||||||
|
unitPlural: 'stackers'
|
||||||
|
})} ${item.mine
|
||||||
|
? `\\ ${numWithUnits(item.meSats, { abbreviate: false })} to post`
|
||||||
|
: `(${numWithUnits(meTotalSats, { abbreviate: false })} from me)`} `}
|
||||||
|
>
|
||||||
|
{numWithUnits(item.sats + pendingSats)}
|
||||||
|
</span>
|
||||||
<span> \ </span>
|
<span> \ </span>
|
||||||
</>}
|
</>}
|
||||||
{item.boost > 0 &&
|
{item.boost > 0 &&
|
||||||
|
@ -57,9 +69,13 @@ export default function ItemInfo ({ item, pendingSats, full, commentsText, class
|
||||||
`/items/${item.id}?commentsViewedAt=${viewedAt}`,
|
`/items/${item.id}?commentsViewedAt=${viewedAt}`,
|
||||||
`/items/${item.id}`)
|
`/items/${item.id}`)
|
||||||
}
|
}
|
||||||
}} title={`${item.commentSats} sats`} className='text-reset position-relative'
|
}} title={numWithUnits(item.commentSats)} className='text-reset position-relative'
|
||||||
>
|
>
|
||||||
{item.ncomments} {commentsText || 'comments'}
|
{numWithUnits(item.ncomments, {
|
||||||
|
abbreviate: false,
|
||||||
|
unitPlural: commentsText,
|
||||||
|
unitSingular: commentTextSingular
|
||||||
|
})}
|
||||||
{hasNewComments &&
|
{hasNewComments &&
|
||||||
<span className={styles.notification}>
|
<span className={styles.notification}>
|
||||||
<span className='invisible'>{' '}</span>
|
<span className='invisible'>{' '}</span>
|
||||||
|
|
|
@ -10,7 +10,7 @@ import BountyIcon from '../svgs/bounty-bag.svg'
|
||||||
import ActionTooltip from './action-tooltip'
|
import ActionTooltip from './action-tooltip'
|
||||||
import Flag from '../svgs/flag-fill.svg'
|
import Flag from '../svgs/flag-fill.svg'
|
||||||
import ImageIcon from '../svgs/image-fill.svg'
|
import ImageIcon from '../svgs/image-fill.svg'
|
||||||
import { abbrNum } from '../lib/format'
|
import { numWithUnits } from '../lib/format'
|
||||||
import ItemInfo from './item-info'
|
import ItemInfo from './item-info'
|
||||||
import { commentsViewedAt } from '../lib/new-comments'
|
import { commentsViewedAt } from '../lib/new-comments'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
@ -58,7 +58,7 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
|
||||||
{item.pollCost && <span className={styles.icon}> <PollIcon className='fill-grey ms-1' height={14} width={14} /></span>}
|
{item.pollCost && <span className={styles.icon}> <PollIcon className='fill-grey ms-1' height={14} width={14} /></span>}
|
||||||
{item.bounty > 0 &&
|
{item.bounty > 0 &&
|
||||||
<span className={styles.icon}>
|
<span className={styles.icon}>
|
||||||
<ActionTooltip notForm overlayText={`${abbrNum(item.bounty)} ${item.bountyPaidTo?.length ? 'sats paid' : 'sats bounty'}`}>
|
<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} />
|
<BountyIcon className={`${styles.bountyIcon} ${item.bountyPaidTo?.length ? 'fill-success' : 'fill-grey'}`} height={16} width={16} />
|
||||||
</ActionTooltip>
|
</ActionTooltip>
|
||||||
</span>}
|
</span>}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
.login {
|
.login {
|
||||||
justify-content: start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -21,6 +21,10 @@ import { useServiceWorker } from './serviceworker'
|
||||||
import { Checkbox, Form } from './form'
|
import { Checkbox, Form } from './form'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useData } from './use-data'
|
import { useData } from './use-data'
|
||||||
|
import { nostrZapDetails } from '../lib/nostr'
|
||||||
|
import Text from './text'
|
||||||
|
import NostrIcon from '../svgs/nostr.svg'
|
||||||
|
import { numWithUnits } from '../lib/format'
|
||||||
|
|
||||||
function Notification ({ n, fresh }) {
|
function Notification ({ n, fresh }) {
|
||||||
const type = n.__typename
|
const type = n.__typename
|
||||||
|
@ -30,7 +34,7 @@ function Notification ({ n, fresh }) {
|
||||||
{
|
{
|
||||||
(type === 'Earn' && <EarnNotification n={n} />) ||
|
(type === 'Earn' && <EarnNotification n={n} />) ||
|
||||||
(type === 'Invitification' && <Invitification n={n} />) ||
|
(type === 'Invitification' && <Invitification n={n} />) ||
|
||||||
(type === 'InvoicePaid' && <InvoicePaid n={n} />) ||
|
(type === 'InvoicePaid' && (n.invoice.nostr ? <NostrZap n={n} /> : <InvoicePaid n={n} />)) ||
|
||||||
(type === 'Referral' && <Referral n={n} />) ||
|
(type === 'Referral' && <Referral n={n} />) ||
|
||||||
(type === 'Streak' && <Streak n={n} />) ||
|
(type === 'Streak' && <Streak n={n} />) ||
|
||||||
(type === 'Votification' && <Votification n={n} />) ||
|
(type === 'Votification' && <Votification n={n} />) ||
|
||||||
|
@ -129,7 +133,11 @@ function Streak ({ n }) {
|
||||||
]
|
]
|
||||||
|
|
||||||
if (n.days) {
|
if (n.days) {
|
||||||
return `After ${n.days} days, ` + LOST_BLURBS[index]
|
return `After ${numWithUnits(n.days, {
|
||||||
|
abbreviate: false,
|
||||||
|
unitSingular: 'day',
|
||||||
|
unitPlural: 'days'
|
||||||
|
})}, ` + LOST_BLURBS[index]
|
||||||
}
|
}
|
||||||
|
|
||||||
return FOUND_BLURBS[index]
|
return FOUND_BLURBS[index]
|
||||||
|
@ -152,14 +160,14 @@ function EarnNotification ({ n }) {
|
||||||
<HandCoin className='align-self-center fill-boost mx-1' width={24} height={24} style={{ flex: '0 0 24px', transform: 'rotateY(180deg)' }} />
|
<HandCoin className='align-self-center fill-boost mx-1' width={24} height={24} style={{ flex: '0 0 24px', transform: 'rotateY(180deg)' }} />
|
||||||
<div className='ms-2'>
|
<div className='ms-2'>
|
||||||
<div className='fw-bold text-boost'>
|
<div className='fw-bold text-boost'>
|
||||||
you stacked {n.earnedSats} sats in rewards<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
you stacked {numWithUnits(n.earnedSats, { abbreviate: false })} in rewards<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
||||||
</div>
|
</div>
|
||||||
{n.sources &&
|
{n.sources &&
|
||||||
<div style={{ fontSize: '80%', color: 'var(--theme-grey)' }}>
|
<div style={{ fontSize: '80%', color: 'var(--theme-grey)' }}>
|
||||||
{n.sources.posts > 0 && <span>{n.sources.posts} sats for top posts</span>}
|
{n.sources.posts > 0 && <span>{numWithUnits(n.sources.posts, { abbreviate: false })} for top posts</span>}
|
||||||
{n.sources.comments > 0 && <span>{n.sources.posts > 0 && ' \\ '}{n.sources.comments} sats for top comments</span>}
|
{n.sources.comments > 0 && <span>{n.sources.posts > 0 && ' \\ '}{numWithUnits(n.sources.comments, { abbreviate: false })} for top comments</span>}
|
||||||
{n.sources.tipPosts > 0 && <span>{(n.sources.comments > 0 || n.sources.posts > 0) && ' \\ '}{n.sources.tipPosts} sats for zapping top posts early</span>}
|
{n.sources.tipPosts > 0 && <span>{(n.sources.comments > 0 || n.sources.posts > 0) && ' \\ '}{numWithUnits(n.sources.tipPosts, { abbreviate: false })} for zapping top posts early</span>}
|
||||||
{n.sources.tipComments > 0 && <span>{(n.sources.comments > 0 || n.sources.posts > 0 || n.sources.tipPosts > 0) && ' \\ '}{n.sources.tipComments} sats for zapping top comments early</span>}
|
{n.sources.tipComments > 0 && <span>{(n.sources.comments > 0 || n.sources.posts > 0 || n.sources.tipPosts > 0) && ' \\ '}{numWithUnits(n.sources.tipComments, { abbreviate: false })} for zapping top comments early</span>}
|
||||||
</div>}
|
</div>}
|
||||||
<div className='pb-1' style={{ lineHeight: '140%' }}>
|
<div className='pb-1' style={{ lineHeight: '140%' }}>
|
||||||
SN distributes the sats it earns back to its best stackers daily. These sats come from <Link href='/~jobs'>jobs</Link>, boosts, posting fees, and donations. You can see the daily rewards pool and make a donation <Link href='/rewards'>here</Link>.
|
SN distributes the sats it earns back to its best stackers daily. These sats come from <Link href='/~jobs'>jobs</Link>, boosts, posting fees, and donations. You can see the daily rewards pool and make a donation <Link href='/rewards'>here</Link>.
|
||||||
|
@ -173,7 +181,12 @@ function Invitification ({ n }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<small className='fw-bold text-secondary ms-2'>
|
<small className='fw-bold text-secondary ms-2'>
|
||||||
your invite has been redeemed by {n.invite.invitees.length} stackers
|
your invite has been redeemed by
|
||||||
|
{numWithUnits(n.invite.invitees.length, {
|
||||||
|
abbreviate: false,
|
||||||
|
unitSingular: 'stacker',
|
||||||
|
unitPlural: 'stackers'
|
||||||
|
})}
|
||||||
</small>
|
</small>
|
||||||
<div className='ms-4 me-2 mt-1'>
|
<div className='ms-4 me-2 mt-1'>
|
||||||
<Invite
|
<Invite
|
||||||
|
@ -187,10 +200,34 @@ function Invitification ({ n }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function NostrZap ({ n }) {
|
||||||
|
const { nostr } = n.invoice
|
||||||
|
const { npub, content, note } = nostrZapDetails(nostr)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='fw-bold text-nostr ms-2 py-1'>
|
||||||
|
<NostrIcon width={24} height={24} className='fill-nostr me-1' />{numWithUnits(n.earnedSats)} zap from
|
||||||
|
<Link className='mx-1 text-reset text-underline' target='_blank' href={`https://snort.social/p/${npub}`} rel='noreferrer'>
|
||||||
|
{npub.slice(0, 10)}...
|
||||||
|
</Link>
|
||||||
|
on {note
|
||||||
|
? (
|
||||||
|
<Link className='mx-1 text-reset text-underline' target='_blank' href={`https://snort.social/e/${note}`} rel='noreferrer'>
|
||||||
|
{note.slice(0, 12)}...
|
||||||
|
</Link>)
|
||||||
|
: 'nostr'}
|
||||||
|
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
||||||
|
{content && <small className='d-block ms-4 ps-1 mt-1 mb-1 text-muted fw-normal'><Text>{content}</Text></small>}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function InvoicePaid ({ n }) {
|
function InvoicePaid ({ n }) {
|
||||||
return (
|
return (
|
||||||
<div className='fw-bold text-info ms-2 py-1'>
|
<div className='fw-bold text-info ms-2 py-1'>
|
||||||
<Check className='fill-info me-1' />{n.earnedSats} sats were deposited in your account
|
<Check className='fill-info me-1' />{numWithUnits(n.earnedSats, { abbreviate: false })} were deposited in your account
|
||||||
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -209,7 +246,7 @@ function Votification ({ n }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<small className='fw-bold text-success ms-2'>
|
<small className='fw-bold text-success ms-2'>
|
||||||
your {n.item.title ? 'post' : 'reply'} {n.item.fwdUser ? 'forwarded' : 'stacked'} {n.earnedSats} sats{n.item.fwdUser && ` to @${n.item.fwdUser.name}`}
|
your {n.item.title ? 'post' : 'reply'} {n.item.fwdUser ? 'forwarded' : 'stacked'} {numWithUnits(n.earnedSats, { abbreviate: false })}{n.item.fwdUser && ` to @${n.item.fwdUser.name}`}
|
||||||
</small>
|
</small>
|
||||||
<div>
|
<div>
|
||||||
{n.item.title
|
{n.item.title
|
||||||
|
|
|
@ -4,7 +4,7 @@ import styles from './pay-bounty.module.css'
|
||||||
import ActionTooltip from './action-tooltip'
|
import ActionTooltip from './action-tooltip'
|
||||||
import { useMutation, gql } from '@apollo/client'
|
import { useMutation, gql } from '@apollo/client'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import { abbrNum } from '../lib/format'
|
import { numWithUnits } from '../lib/format'
|
||||||
import { useShowModal } from './modal'
|
import { useShowModal } from './modal'
|
||||||
import FundError from './fund-error'
|
import FundError from './fund-error'
|
||||||
import { useRoot } from './root'
|
import { useRoot } from './root'
|
||||||
|
@ -90,7 +90,7 @@ export default function PayBounty ({ children, item }) {
|
||||||
return (
|
return (
|
||||||
<ActionTooltip
|
<ActionTooltip
|
||||||
notForm
|
notForm
|
||||||
overlayText={`${root.bounty} sats`}
|
overlayText={numWithUnits(root.bounty)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={styles.pay} onClick={() => {
|
className={styles.pay} onClick={() => {
|
||||||
|
@ -101,7 +101,7 @@ export default function PayBounty ({ children, item }) {
|
||||||
</div>
|
</div>
|
||||||
<div className='text-center'>
|
<div className='text-center'>
|
||||||
<Button className='mt-4' variant='primary' onClick={() => handlePayBounty(onClose)}>
|
<Button className='mt-4' variant='primary' onClick={() => handlePayBounty(onClose)}>
|
||||||
pay <small>{abbrNum(root.bounty)} sats</small>
|
pay <small>{numWithUnits(root.bounty)}</small>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { gql, useMutation } from '@apollo/client'
|
import { gql, useMutation } from '@apollo/client'
|
||||||
import Button from 'react-bootstrap/Button'
|
import Button from 'react-bootstrap/Button'
|
||||||
import { fixedDecimal } from '../lib/format'
|
import { fixedDecimal, numWithUnits } from '../lib/format'
|
||||||
import { timeLeft } from '../lib/time'
|
import { timeLeft } from '../lib/time'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import styles from './poll.module.css'
|
import styles from './poll.module.css'
|
||||||
|
@ -86,7 +86,7 @@ export default function Poll ({ item }) {
|
||||||
key={v.id} v={v}
|
key={v.id} v={v}
|
||||||
progress={item.poll.count ? fixedDecimal(v.count * 100 / item.poll.count, 1) : 0}
|
progress={item.poll.count ? fixedDecimal(v.count * 100 / item.poll.count, 1) : 0}
|
||||||
/>)}
|
/>)}
|
||||||
<div className='text-muted mt-1'>{item.poll.count} votes \ {expiresIn ? `${expiresIn} left` : 'poll ended'}</div>
|
<div className='text-muted mt-1'>{numWithUnits(item.poll.count, { unitSingular: 'vote', unitPlural: 'votes' })} \ {expiresIn ? `${expiresIn} left` : 'poll ended'}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { NextSeo } from 'next-seo'
|
import { NextSeo } from 'next-seo'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import removeMd from 'remove-markdown'
|
import removeMd from 'remove-markdown'
|
||||||
|
import { numWithUnits } from '../lib/format'
|
||||||
|
|
||||||
export function SeoSearch ({ sub }) {
|
export function SeoSearch ({ sub }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -55,10 +56,10 @@ export default function Seo ({ sub, item, user }) {
|
||||||
desc = desc.replace(/\s+/g, ' ')
|
desc = desc.replace(/\s+/g, ' ')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
desc = `@${item.user.name} stacked ${item.sats} sats ${item.url ? `posting ${item.url}` : 'with this discussion'}`
|
desc = `@${item.user.name} stacked ${numWithUnits(item.sats)} ${item.url ? `posting ${item.url}` : 'with this discussion'}`
|
||||||
}
|
}
|
||||||
if (item.ncomments) {
|
if (item.ncomments) {
|
||||||
desc += ` [${item.ncomments} comments`
|
desc += ` [${numWithUnits(item.ncomments, { unitSingular: 'comment', unitPlural: 'comments' })}`
|
||||||
if (item.boost) {
|
if (item.boost) {
|
||||||
desc += `, ${item.boost} boost`
|
desc += `, ${item.boost} boost`
|
||||||
}
|
}
|
||||||
|
@ -68,7 +69,7 @@ export default function Seo ({ sub, item, user }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (user) {
|
if (user) {
|
||||||
desc = `@${user.name} has [${user.stacked} stacked, ${user.nposts} posts, ${user.ncomments} comments]`
|
desc = `@${user.name} has [${user.stacked} stacked, ${numWithUnits(user.nitems, { unitSingular: 'item', unitPlural: 'items' })}]`
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -99,7 +99,7 @@ export const ServiceWorkerProvider = ({ children }) => {
|
||||||
// since (a lot of) browsers don't support the pushsubscriptionchange event,
|
// since (a lot of) browsers don't support the pushsubscriptionchange event,
|
||||||
// we sync with server manually by checking on every page reload if the push subscription changed.
|
// we sync with server manually by checking on every page reload if the push subscription changed.
|
||||||
// see https://medium.com/@madridserginho/how-to-handle-webpush-api-pushsubscriptionchange-event-in-modern-browsers-6e47840d756f
|
// see https://medium.com/@madridserginho/how-to-handle-webpush-api-pushsubscriptionchange-event-in-modern-browsers-6e47840d756f
|
||||||
navigator.serviceWorker.controller.postMessage({ action: 'SYNC_SUBSCRIPTION' })
|
navigator?.serviceWorker?.controller?.postMessage?.({ action: 'SYNC_SUBSCRIPTION' })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import Popover from 'react-bootstrap/Popover'
|
||||||
import { useShowModal } from './modal'
|
import { useShowModal } from './modal'
|
||||||
import { LightningConsumer, useLightning } from './lightning'
|
import { LightningConsumer, useLightning } from './lightning'
|
||||||
import { isInsufficientFundsError } from '../lib/anonymous'
|
import { isInsufficientFundsError } from '../lib/anonymous'
|
||||||
|
import { numWithUnits } from '../lib/format'
|
||||||
|
|
||||||
const getColor = (meSats) => {
|
const getColor = (meSats) => {
|
||||||
if (!meSats || meSats <= 10) {
|
if (!meSats || meSats <= 10) {
|
||||||
|
@ -223,7 +224,7 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
|
||||||
sats = raiseTip - meSats
|
sats = raiseTip - meSats
|
||||||
}
|
}
|
||||||
|
|
||||||
return [meSats, meTotalSats, sats, `${sats} sat${sats > 1 ? 's' : ''}`, getColor(meTotalSats)]
|
return [meSats, meTotalSats, sats, numWithUnits(sats, { abbreviate: false }), getColor(meSats)]
|
||||||
}, [item?.meSats, item?.meAnonSats, pendingSats, me?.tipDefault, me?.turboDefault])
|
}, [item?.meSats, item?.meAnonSats, pendingSats, me?.tipDefault, me?.turboDefault])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -17,6 +17,7 @@ import Avatar from './avatar'
|
||||||
import CowboyHat from './cowboy-hat'
|
import CowboyHat from './cowboy-hat'
|
||||||
import { userSchema } from '../lib/validate'
|
import { userSchema } from '../lib/validate'
|
||||||
import { useShowModal } from './modal'
|
import { useShowModal } from './modal'
|
||||||
|
import { numWithUnits } from '../lib/format'
|
||||||
|
|
||||||
export default function UserHeader ({ user }) {
|
export default function UserHeader ({ user }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -35,7 +36,13 @@ export default function UserHeader ({ user }) {
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Link href={'/' + user.name + '/all'} passHref legacyBehavior>
|
<Link href={'/' + user.name + '/all'} passHref legacyBehavior>
|
||||||
<Nav.Link eventKey>{user.nitems} items</Nav.Link>
|
<Nav.Link eventKey>
|
||||||
|
{numWithUnits(user.nitems, {
|
||||||
|
abbreviate: false,
|
||||||
|
unitSingular: 'item',
|
||||||
|
unitPlural: 'items'
|
||||||
|
})}
|
||||||
|
</Nav.Link>
|
||||||
</Link>
|
</Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
</Nav>
|
</Nav>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Image from 'react-bootstrap/Image'
|
import Image from 'react-bootstrap/Image'
|
||||||
import { abbrNum } from '../lib/format'
|
import { abbrNum, numWithUnits } from '../lib/format'
|
||||||
import CowboyHat from './cowboy-hat'
|
import CowboyHat from './cowboy-hat'
|
||||||
import styles from './item.module.css'
|
import styles from './item.module.css'
|
||||||
import userStyles from './user-header.module.css'
|
import userStyles from './user-header.module.css'
|
||||||
|
@ -14,13 +14,13 @@ const Stacked = ({ user }) => (<span>{abbrNum(user.stacked)} stacked</span>)
|
||||||
const Spent = ({ user }) => (<span>{abbrNum(user.spent)} spent</span>)
|
const Spent = ({ user }) => (<span>{abbrNum(user.spent)} spent</span>)
|
||||||
const Posts = ({ user }) => (
|
const Posts = ({ user }) => (
|
||||||
<Link href={`/${user.name}/posts`} className='text-reset'>
|
<Link href={`/${user.name}/posts`} className='text-reset'>
|
||||||
{abbrNum(user.nposts)} posts
|
{numWithUnits(user.nposts, { unitSingular: 'post', unitPlural: 'posts' })}
|
||||||
</Link>)
|
</Link>)
|
||||||
const Comments = ({ user }) => (
|
const Comments = ({ user }) => (
|
||||||
<Link href={`/${user.name}/comments`} className='text-reset'>
|
<Link href={`/${user.name}/comments`} className='text-reset'>
|
||||||
{abbrNum(user.ncomments)} comments
|
{numWithUnits(user.ncomments, { unitSingular: 'comment', unitPlural: 'comments' })}
|
||||||
</Link>)
|
</Link>)
|
||||||
const Referrals = ({ user }) => (<span>{abbrNum(user.referrals)} referrals</span>)
|
const Referrals = ({ user }) => (<span>{numWithUnits(user.referrals, { unitSingular: 'referral', unitPlural: 'referrals' })}</span>)
|
||||||
const Seperator = () => (<span> \ </span>)
|
const Seperator = () => (<span> \ </span>)
|
||||||
|
|
||||||
const STAT_POS = {
|
const STAT_POS = {
|
||||||
|
|
|
@ -80,6 +80,7 @@ export const NOTIFICATIONS = gql`
|
||||||
earnedSats
|
earnedSats
|
||||||
invoice {
|
invoice {
|
||||||
id
|
id
|
||||||
|
nostr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ export const INVOICE = gql`
|
||||||
cancelled
|
cancelled
|
||||||
confirmedAt
|
confirmedAt
|
||||||
expiresAt
|
expiresAt
|
||||||
|
nostr
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,27 @@ export const abbrNum = n => {
|
||||||
if (n >= 1e12) return +(n / 1e12).toFixed(1) + 't'
|
if (n >= 1e12) return +(n / 1e12).toFixed(1) + 't'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take a number that represents a count
|
||||||
|
* and return a formatted label e.g. 0 sats, 1 sat, 2 sats
|
||||||
|
*
|
||||||
|
* @param n The number of sats
|
||||||
|
* @param opts Options
|
||||||
|
* @param opts.abbreviate Whether to abbreviate the number
|
||||||
|
* @param opts.unitSingular The singular unit label
|
||||||
|
* @param opts.unitPlural The plural unit label
|
||||||
|
*/
|
||||||
|
export const numWithUnits = (n, {
|
||||||
|
abbreviate = true,
|
||||||
|
unitSingular = 'sat',
|
||||||
|
unitPlural = 'sats'
|
||||||
|
} = {}) => {
|
||||||
|
if (isNaN(n)) {
|
||||||
|
return `${n} ${unitPlural}`
|
||||||
|
}
|
||||||
|
return `${abbreviate ? abbrNum(n) : n} ${n === 1 ? unitSingular : unitPlural}`
|
||||||
|
}
|
||||||
|
|
||||||
export const fixedDecimal = (n, f) => {
|
export const fixedDecimal = (n, f) => {
|
||||||
return Number.parseFloat(n).toFixed(f)
|
return Number.parseFloat(n).toFixed(f)
|
||||||
}
|
}
|
||||||
|
|
24
lib/nostr.js
24
lib/nostr.js
|
@ -1,3 +1,27 @@
|
||||||
|
import { bech32 } from 'bech32'
|
||||||
|
|
||||||
export const NOSTR_PUBKEY_HEX = /^[0-9a-fA-F]{64}$/
|
export const NOSTR_PUBKEY_HEX = /^[0-9a-fA-F]{64}$/
|
||||||
export const NOSTR_PUBKEY_BECH32 = /^npub1[02-9ac-hj-np-z]+$/
|
export const NOSTR_PUBKEY_BECH32 = /^npub1[02-9ac-hj-np-z]+$/
|
||||||
export const NOSTR_MAX_RELAY_NUM = 20
|
export const NOSTR_MAX_RELAY_NUM = 20
|
||||||
|
export const NOSTR_ZAPPLE_PAY_NPUB = 'npub1wxl6njlcgygduct7jkgzrvyvd9fylj4pqvll6p32h59wyetm5fxqjchcan'
|
||||||
|
|
||||||
|
export function hexToBech32 (hex, prefix = 'npub') {
|
||||||
|
return bech32.encode(prefix, bech32.toWords(Buffer.from(hex, 'hex')))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nostrZapDetails (zap) {
|
||||||
|
let { pubkey, content, tags } = zap
|
||||||
|
let npub = hexToBech32(pubkey)
|
||||||
|
if (npub === NOSTR_ZAPPLE_PAY_NPUB) {
|
||||||
|
const znpub = content.match(/^From: nostr:(npub1[02-9ac-hj-np-z]+)$/)?.[1]
|
||||||
|
if (znpub) {
|
||||||
|
npub = znpub
|
||||||
|
// zapple pay does not support user content
|
||||||
|
content = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const event = tags.filter(t => t?.length >= 2 && t[0] === 'e')?.[0]?.[1]
|
||||||
|
const note = event ? hexToBech32(event, 'note') : null
|
||||||
|
|
||||||
|
return { npub, content, note }
|
||||||
|
}
|
||||||
|
|
|
@ -14,10 +14,18 @@ const corsHeaders = [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
// XXX this fragile ... eb could change the version label ... but it works for now
|
let commitHash
|
||||||
const commitHash = isProd
|
if (isProd) {
|
||||||
? Object.keys(require('/opt/elasticbeanstalk/deployment/app_version_manifest.json').RuntimeSources['stacker.news'])[0].match(/^app-(.+)-/)[1] // eslint-disable-line
|
// XXX this fragile ... eb could change the version label ... but it works for now
|
||||||
: require('child_process').execSync('git rev-parse HEAD').toString().slice(0, 4)
|
commitHash = Object.keys(require('/opt/elasticbeanstalk/deployment/app_version_manifest.json').RuntimeSources['stacker.news'])[0].match(/^app-(.+)-/)[1] // eslint-disable-line
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
commitHash = require('child_process').execSync('git rev-parse HEAD').toString().slice(0, 4)
|
||||||
|
} catch (e) {
|
||||||
|
console.log('could not get commit hash with `git rev-parse HEAD` ... using 0000')
|
||||||
|
commitHash = '0000'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = withPlausibleProxy()({
|
module.exports = withPlausibleProxy()({
|
||||||
env: {
|
env: {
|
||||||
|
|
|
@ -74,8 +74,8 @@
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remove-markdown": "^0.5.0",
|
"remove-markdown": "^0.5.0",
|
||||||
"sass": "^1.64.1",
|
"sass": "^1.64.1",
|
||||||
"tldts": "^6.0.13",
|
|
||||||
"serviceworker-storage": "^0.1.0",
|
"serviceworker-storage": "^0.1.0",
|
||||||
|
"tldts": "^6.0.13",
|
||||||
"typescript": "^5.1.6",
|
"typescript": "^5.1.6",
|
||||||
"unist-util-visit": "^5.0.0",
|
"unist-util-visit": "^5.0.0",
|
||||||
"url-unshort": "^6.1.0",
|
"url-unshort": "^6.1.0",
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { useQuery } from '@apollo/client'
|
||||||
import PageLoading from '../../components/page-loading'
|
import PageLoading from '../../components/page-loading'
|
||||||
import { WHENS } from '../../lib/constants'
|
import { WHENS } from '../../lib/constants'
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
|
import { numWithUnits } from '../../lib/format'
|
||||||
|
|
||||||
const WhenComposedChart = dynamic(() => import('../../components/charts').then(mod => mod.WhenComposedChart), {
|
const WhenComposedChart = dynamic(() => import('../../components/charts').then(mod => mod.WhenComposedChart), {
|
||||||
loading: () => <div>Loading...</div>
|
loading: () => <div>Loading...</div>
|
||||||
|
@ -44,7 +45,7 @@ export default function Referrals ({ ssrData }) {
|
||||||
return (
|
return (
|
||||||
<CenterLayout footerLinks>
|
<CenterLayout footerLinks>
|
||||||
<h4 className='fw-bold text-muted text-center pt-5 pb-3 d-flex align-items-center justify-content-center'>
|
<h4 className='fw-bold text-muted text-center pt-5 pb-3 d-flex align-items-center justify-content-center'>
|
||||||
{totalReferrals} referrals & {totalSats} sats in the last
|
{numWithUnits(totalReferrals, { unitPlural: 'referrals', unitSingular: 'referral' })} & {numWithUnits(totalSats, { abbreviate: false })} in the last
|
||||||
<Select
|
<Select
|
||||||
groupClassName='mb-0 ms-2'
|
groupClassName='mb-0 ms-2'
|
||||||
className='w-auto'
|
className='w-auto'
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { useMutation, useQuery } from '@apollo/client'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { amountSchema } from '../lib/validate'
|
import { amountSchema } from '../lib/validate'
|
||||||
import Countdown from 'react-countdown'
|
import Countdown from 'react-countdown'
|
||||||
import { abbrNum } from '../lib/format'
|
import { numWithUnits } from '../lib/format'
|
||||||
import PageLoading from '../components/page-loading'
|
import PageLoading from '../components/page-loading'
|
||||||
import { useShowModal } from '../components/modal'
|
import { useShowModal } from '../components/modal'
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
|
@ -52,7 +52,7 @@ export function RewardLine ({ total }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{abbrNum(total)} sats in rewards
|
{numWithUnits(total)} in rewards
|
||||||
{threshold &&
|
{threshold &&
|
||||||
<Countdown
|
<Countdown
|
||||||
date={threshold}
|
date={threshold}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { CREATE_WITHDRAWL, SEND_TO_LNADDR } from '../fragments/wallet'
|
||||||
import { getGetServerSideProps } from '../api/ssrApollo'
|
import { getGetServerSideProps } from '../api/ssrApollo'
|
||||||
import { amountSchema, lnAddrSchema, withdrawlSchema } from '../lib/validate'
|
import { amountSchema, lnAddrSchema, withdrawlSchema } from '../lib/validate'
|
||||||
import { SSR } from '../lib/constants'
|
import { SSR } from '../lib/constants'
|
||||||
|
import { numWithUnits } from '../lib/format'
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps()
|
export const getServerSideProps = getGetServerSideProps()
|
||||||
|
|
||||||
|
@ -30,7 +31,7 @@ function YouHaveSats () {
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
return (
|
return (
|
||||||
<h2 className={`${me ? 'visible' : 'invisible'} text-success pb-5`}>
|
<h2 className={`${me ? 'visible' : 'invisible'} text-success pb-5`}>
|
||||||
you have <span className='text-monospace'>{me && me.sats}</span> sats
|
you have <span className='text-monospace'>{me && numWithUnits(me.sats, { abbreviate: false })}</span>
|
||||||
</h2>
|
</h2>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { useRouter } from 'next/router'
|
||||||
import { WITHDRAWL } from '../../fragments/wallet'
|
import { WITHDRAWL } from '../../fragments/wallet'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { SSR } from '../../lib/constants'
|
import { SSR } from '../../lib/constants'
|
||||||
|
import { numWithUnits } from '../../lib/format'
|
||||||
|
|
||||||
export default function Withdrawl () {
|
export default function Withdrawl () {
|
||||||
return (
|
return (
|
||||||
|
@ -53,7 +54,7 @@ function LoadWithdrawl () {
|
||||||
let variant = 'default'
|
let variant = 'default'
|
||||||
switch (data.withdrawl.status) {
|
switch (data.withdrawl.status) {
|
||||||
case 'CONFIRMED':
|
case 'CONFIRMED':
|
||||||
status = `sent ${data.withdrawl.satsPaid} sats with ${data.withdrawl.satsFeePaid} sats in routing fees`
|
status = `sent ${numWithUnits(data.withdrawl.satsPaid, { abbreviate: false })} with ${numWithUnits(data.withdrawl.satsFeePaid, { abbreviate: false })} in routing fees`
|
||||||
variant = 'confirmed'
|
variant = 'confirmed'
|
||||||
break
|
break
|
||||||
case 'INSUFFICIENT_BALANCE':
|
case 'INSUFFICIENT_BALANCE':
|
||||||
|
|
|
@ -608,6 +608,10 @@ div[contenteditable]:focus,
|
||||||
fill: var(--theme-grey);
|
fill: var(--theme-grey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fill-nostr {
|
||||||
|
fill: var(--bs-nostr);
|
||||||
|
}
|
||||||
|
|
||||||
.fill-lgrey {
|
.fill-lgrey {
|
||||||
fill: #a5a5a5;
|
fill: #a5a5a5;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { enable } from 'workbox-navigation-preload'
|
||||||
import manifest from './precache-manifest.json'
|
import manifest from './precache-manifest.json'
|
||||||
import ServiceWorkerStorage from 'serviceworker-storage'
|
import ServiceWorkerStorage from 'serviceworker-storage'
|
||||||
|
|
||||||
|
// comment out to enable workbox console logs
|
||||||
self.__WB_DISABLE_DEV_LOGS = true
|
self.__WB_DISABLE_DEV_LOGS = true
|
||||||
|
|
||||||
const storage = new ServiceWorkerStorage('sw:storage', 1)
|
const storage = new ServiceWorkerStorage('sw:storage', 1)
|
||||||
|
@ -15,9 +16,6 @@ const storage = new ServiceWorkerStorage('sw:storage', 1)
|
||||||
// https://developer.chrome.com/docs/workbox/modules/workbox-navigation-preload/
|
// https://developer.chrome.com/docs/workbox/modules/workbox-navigation-preload/
|
||||||
enable()
|
enable()
|
||||||
|
|
||||||
// uncomment to disable workbox console logs
|
|
||||||
// self.__WB_DISABLE_DEV_LOGS = true
|
|
||||||
|
|
||||||
// ignore precache manifest generated by InjectManifest
|
// ignore precache manifest generated by InjectManifest
|
||||||
// they statically check for the presence of this variable
|
// they statically check for the presence of this variable
|
||||||
console.log(self.__WB_MANIFEST)
|
console.log(self.__WB_MANIFEST)
|
||||||
|
|
Loading…
Reference in New Issue