diff --git a/api/resolvers/item.js b/api/resolvers/item.js index c0aa5bfe..b49e6de7 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -9,7 +9,7 @@ import { MAX_TITLE_LENGTH, ITEM_FILTER_THRESHOLD, DONT_LIKE_THIS_COST, COMMENT_DEPTH_LIMIT, COMMENT_TYPE_QUERY } from '../../lib/constants' -import { msatsToSats } from '../../lib/format' +import { msatsToSats, numWithUnits } from '../../lib/format' import { parse } from 'tldts' import uu from 'url-unshort' import { amountSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '../../lib/validate' @@ -731,7 +731,7 @@ export default { const [{ item_act: vote }] = await serialize(models, models.$queryRaw`SELECT item_act(${Number(id)}::INTEGER, ${me.id}::INTEGER, 'TIP', ${Number(sats)}::INTEGER)`) 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, { title, body: updatedItem.title ? updatedItem.title : updatedItem.text, diff --git a/components/adv-post-form.js b/components/adv-post-form.js index 6a6dfcd9..4d6ade10 100644 --- a/components/adv-post-form.js +++ b/components/adv-post-form.js @@ -3,6 +3,7 @@ import { Input, InputUserSuggest } from './form' import InputGroup from 'react-bootstrap/InputGroup' import { BOOST_MIN } from '../lib/constants' import Info from './info' +import { numWithUnits } from '../lib/format' export function AdvPostInitial ({ forward }) { return { @@ -23,10 +24,10 @@ export default function AdvPostForm ({ edit }) {
  1. Boost ranks posts higher temporarily based on the amount
  2. -
  3. The minimum boost is {BOOST_MIN} sats
  4. -
  5. Each {BOOST_MIN} sats of boost is equivalent to one trusted upvote +
  6. The minimum boost is {numWithUnits(BOOST_MIN, { abbreviate: false })}
  7. +
  8. Each {numWithUnits(BOOST_MIN, { abbreviate: false })} of boost is equivalent to one trusted upvote
      -
    • e.g. {BOOST_MIN * 2} sats is like 2 votes
    • +
    • e.g. {numWithUnits(BOOST_MIN * 2, { abbreviate: false })} is like 2 votes
  9. The decay of boost "votes" increases at 2x the rate of organic votes diff --git a/components/comment.js b/components/comment.js index a016ca97..f3a4b1fe 100644 --- a/components/comment.js +++ b/components/comment.js @@ -15,7 +15,7 @@ import PayBounty from './pay-bounty' import BountyIcon from '../svgs/bounty-bag.svg' import ActionTooltip from './action-tooltip' import Flag from '../svgs/flag-fill.svg' -import { abbrNum } from '../lib/format' +import { numWithUnits } from '../lib/format' import Share from './share' import ItemInfo from './item-info' import Badge from 'react-bootstrap/Badge' @@ -154,7 +154,7 @@ export default function Comment ({ <> {includeParent && } {bountyPaid && - + } diff --git a/components/comments.js b/components/comments.js index d8a22455..55a492dd 100644 --- a/components/comments.js +++ b/components/comments.js @@ -2,7 +2,7 @@ import Comment, { CommentSkeleton } from './comment' import styles from './header.module.css' import Nav from 'react-bootstrap/Nav' import Navbar from 'react-bootstrap/Navbar' -import { abbrNum } from '../lib/format' +import { numWithUnits } from '../lib/format' import { defaultCommentSort } from '../lib/item' import { useRouter } from 'next/router' @@ -23,7 +23,7 @@ export function CommentsHeader ({ handleSort, pinned, bio, parentCreatedAt, comm activeKey={sort} > - {abbrNum(commentSats)} sats + {numWithUnits(commentSats)}
    diff --git a/components/fee-button.js b/components/fee-button.js index 7bb11a23..bed62826 100644 --- a/components/fee-button.js +++ b/components/fee-button.js @@ -5,13 +5,14 @@ import styles from './fee-button.module.css' import { gql, useQuery } from '@apollo/client' import { useFormikContext } from 'formik' import { SSR } from '../lib/constants' +import { numWithUnits } from '../lib/format' function Receipt ({ cost, repetition, hasImgLink, baseFee, parentId, boost }) { return ( - + {hasImgLink && @@ -26,13 +27,13 @@ function Receipt ({ cost, repetition, hasImgLink, baseFee, parentId, boost }) { } {boost > 0 && - + } - + @@ -53,8 +54,8 @@ export default function FeeButton ({ parentId, hasImgLink, baseFee, ChildButton, const show = alwaysShow || !formik?.isSubmitting return (
    - - {text}{cost > baseFee && show && {cost} sats} + + {text}{cost > baseFee && show && {numWithUnits(cost, { abbreviate: false })}} {cost > baseFee && show && @@ -71,7 +72,7 @@ function EditReceipt ({ cost, paidSats, addImgLink, boost, parentId }) { {addImgLink && <>
    - + @@ -79,19 +80,19 @@ function EditReceipt ({ cost, paidSats, addImgLink, boost, parentId }) { - + } {boost > 0 && - + } - + @@ -108,8 +109,8 @@ export function EditFeeButton ({ paidSats, hadImgLink, hasImgLink, ChildButton, const show = alwaysShow || !formik?.isSubmitting return (
    - - {text}{cost > 0 && show && {cost} sats} + + {text}{cost > 0 && show && {numWithUnits(cost, { abbreviate: false })}} {cost > 0 && show && diff --git a/components/invoice.js b/components/invoice.js index 3c81d53a..4c0ddade 100644 --- a/components/invoice.js +++ b/components/invoice.js @@ -1,5 +1,6 @@ import AccordianItem from './accordian-item' import Qr from './qr' +import { numWithUnits } from '../lib/format' export function Invoice ({ invoice }) { let variant = 'default' @@ -7,7 +8,7 @@ export function Invoice ({ invoice }) { let webLn = true if (invoice.confirmedAt) { variant = 'confirmed' - status = `${invoice.satsReceived} sats deposited` + status = `${numWithUnits(invoice.satsReceived, { abbreviate: false })} deposited` webLn = false } else if (invoice.cancelled) { variant = 'failed' diff --git a/components/item-full.js b/components/item-full.js index b279e603..2e2d0e1f 100644 --- a/components/item-full.js +++ b/components/item-full.js @@ -23,6 +23,7 @@ import Toc from './table-of-contents' import Link from 'next/link' import { RootProvider } from './root' import { IMGPROXY_URL_REGEXP } from '../lib/url' +import { numWithUnits } from '../lib/format' function BioItem ({ item, handleClick }) { const me = useMe() @@ -139,11 +140,11 @@ function TopLevelItem ({ item, noReply, ...props }) { {item.bountyPaidTo?.length ? (
    - {item.bounty} sats paid + {numWithUnits(item.bounty, { abbreviate: false })} paid
    ) : (
    - {item.bounty} sats bounty + {numWithUnits(item.bounty, { abbreviate: false })} bounty
    )}
    } diff --git a/components/item-info.js b/components/item-info.js index 1c23308e..fd24eff7 100644 --- a/components/item-info.js +++ b/components/item-info.js @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react' import Badge from 'react-bootstrap/Badge' import Dropdown from 'react-bootstrap/Dropdown' import Countdown from './countdown' -import { abbrNum } from '../lib/format' +import { abbrNum, numWithUnits } from '../lib/format' import { newComments, commentsViewedAt } from '../lib/new-comments' import { timeSince } from '../lib/time' import CowboyHat from './cowboy-hat' @@ -34,7 +34,12 @@ export default function ItemInfo ({ item, pendingSats, full, commentsText, class
    {!item.position && <> - {abbrNum(item.sats + pendingSats)} sats + + {numWithUnits(item.sats + pendingSats)} + \ } {item.boost > 0 && @@ -51,7 +56,7 @@ export default function ItemInfo ({ item, pendingSats, full, commentsText, class `/items/${item.id}?commentsViewedAt=${viewedAt}`, `/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'} {hasNewComments && diff --git a/components/item.js b/components/item.js index d63444bc..ded16dd3 100644 --- a/components/item.js +++ b/components/item.js @@ -10,7 +10,7 @@ import BountyIcon from '../svgs/bounty-bag.svg' import ActionTooltip from './action-tooltip' import Flag from '../svgs/flag-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 { commentsViewedAt } from '../lib/new-comments' import { useRouter } from 'next/router' @@ -58,7 +58,7 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s {item.pollCost && } {item.bounty > 0 && - + } diff --git a/components/notifications.js b/components/notifications.js index cd943854..593a570a 100644 --- a/components/notifications.js +++ b/components/notifications.js @@ -24,6 +24,7 @@ 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 }) { const type = n.__typename @@ -155,14 +156,14 @@ function EarnNotification ({ n }) {
    - you stacked {n.earnedSats} sats in rewards{timeSince(new Date(n.sortTime))} + you stacked {numWithUnits(n.earnedSats, { abbreviate: false })} in rewards{timeSince(new Date(n.sortTime))}
    {n.sources &&
    - {n.sources.posts > 0 && {n.sources.posts} sats for top posts} - {n.sources.comments > 0 && {n.sources.posts > 0 && ' \\ '}{n.sources.comments} sats for top comments} - {n.sources.tipPosts > 0 && {(n.sources.comments > 0 || n.sources.posts > 0) && ' \\ '}{n.sources.tipPosts} sats for zapping top posts early} - {n.sources.tipComments > 0 && {(n.sources.comments > 0 || n.sources.posts > 0 || n.sources.tipPosts > 0) && ' \\ '}{n.sources.tipComments} sats for zapping top comments early} + {n.sources.posts > 0 && {numWithUnits(n.sources.posts, { abbreviate: false })} for top posts} + {n.sources.comments > 0 && {n.sources.posts > 0 && ' \\ '}{numWithUnits(n.sources.comments, { abbreviate: false })} for top comments} + {n.sources.tipPosts > 0 && {(n.sources.comments > 0 || n.sources.posts > 0) && ' \\ '}{numWithUnits(n.sources.tipPosts, { abbreviate: false })} for zapping top posts early} + {n.sources.tipComments > 0 && {(n.sources.comments > 0 || n.sources.posts > 0 || n.sources.tipPosts > 0) && ' \\ '}{numWithUnits(n.sources.tipComments, { abbreviate: false })} for zapping top comments early}
    }
    SN distributes the sats it earns back to its best stackers daily. These sats come from jobs, boosts, posting fees, and donations. You can see the daily rewards pool and make a donation here. @@ -217,7 +218,7 @@ function NostrZap ({ n }) { function InvoicePaid ({ n }) { return (
    - {n.earnedSats} sats were deposited in your account + {numWithUnits(n.earnedSats, { abbreviate: false })} were deposited in your account {timeSince(new Date(n.sortTime))}
    ) @@ -236,7 +237,7 @@ function Votification ({ n }) { return ( <> - 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}`}
    {n.item.title diff --git a/components/pay-bounty.js b/components/pay-bounty.js index 943a7c85..22420c85 100644 --- a/components/pay-bounty.js +++ b/components/pay-bounty.js @@ -4,7 +4,7 @@ import styles from './pay-bounty.module.css' import ActionTooltip from './action-tooltip' import { useMutation, gql } from '@apollo/client' import { useMe } from './me' -import { abbrNum } from '../lib/format' +import { numWithUnits } from '../lib/format' import { useShowModal } from './modal' import FundError from './fund-error' import { useRoot } from './root' @@ -90,7 +90,7 @@ export default function PayBounty ({ children, item }) { return (
    { @@ -101,7 +101,7 @@ export default function PayBounty ({ children, item }) {
    diff --git a/components/seo.js b/components/seo.js index a927f5b9..839da65c 100644 --- a/components/seo.js +++ b/components/seo.js @@ -1,6 +1,7 @@ import { NextSeo } from 'next-seo' import { useRouter } from 'next/router' import removeMd from 'remove-markdown' +import { numWithUnits } from '../lib/format' export function SeoSearch ({ sub }) { const router = useRouter() @@ -55,7 +56,7 @@ export default function Seo ({ sub, item, user }) { desc = desc.replace(/\s+/g, ' ') } } 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) { desc += ` [${item.ncomments} comments` diff --git a/lib/format.js b/lib/format.js index 4e2ef67f..2bcd0763 100644 --- a/lib/format.js +++ b/lib/format.js @@ -6,6 +6,27 @@ export const abbrNum = n => { 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) => { return Number.parseFloat(n).toFixed(f) } diff --git a/pages/referrals/[when].js b/pages/referrals/[when].js index 6012b952..420736b1 100644 --- a/pages/referrals/[when].js +++ b/pages/referrals/[when].js @@ -9,6 +9,7 @@ import { useQuery } from '@apollo/client' import PageLoading from '../../components/page-loading' import { WHENS } from '../../lib/constants' import dynamic from 'next/dynamic' +import { numWithUnits } from '../../lib/format' const WhenComposedChart = dynamic(() => import('../../components/charts').then(mod => mod.WhenComposedChart), { loading: () =>
    Loading...
    @@ -44,7 +45,7 @@ export default function Referrals ({ ssrData }) { return (

    - {totalReferrals} referrals & {totalSats} sats in the last + {totalReferrals} referrals & {numWithUnits(totalSats, { abbreviate: false })} in the last

    {baseFee} sats{numWithUnits(baseFee, { abbreviate: false })} {parentId ? 'reply' : 'post'} fee
    + {boost} sats+ {numWithUnits(boost, { abbreviate: false })} boost
    {cost} sats{numWithUnits(cost, { abbreviate: false })} total fee
    {paidSats} sats{numWithUnits(paidSats, { abbreviate: false })} {parentId ? 'reply' : 'post'} fee
    image/link fee
    - {paidSats} sats- {numWithUnits(paidSats, { abbreviate: false })} already paid
    + {boost} sats+ {numWithUnits(boost, { abbreviate: false })} boost
    {cost} sats{numWithUnits(cost)} total fee