import { useState, useEffect, useMemo } from 'react' import { useQuery } from '@apollo/client' import Comment, { CommentSkeleton } from './comment' import Item from './item' import ItemJob from './item-job' import { NOTIFICATIONS } from '../fragments/notifications' import MoreFooter from './more-footer' import Invite from './invite' import { ignoreClick } from '../lib/clicks' import { dayMonthYear, timeSince } from '../lib/time' import Link from 'next/link' import Check from '../svgs/check-double-line.svg' import HandCoin from '../svgs/hand-coin-fill.svg' import { LOST_BLURBS, FOUND_BLURBS } from '../lib/constants' import CowboyHatIcon from '../svgs/cowboy.svg' import BaldIcon from '../svgs/bald.svg' import { RootProvider } from './root' import Alert from 'react-bootstrap/Alert' import styles from './notifications.module.css' import { useServiceWorker } from './serviceworker' import { Checkbox, Form } from './form' import { useRouter } from 'next/router' 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' import BountyIcon from '../svgs/bounty-bag.svg' import { LongCountdown } from './countdown' import { nextBillingWithGrace } from '../lib/territory' import { commentSubTreeRootId } from '../lib/item' function Notification ({ n, fresh }) { const type = n.__typename return ( { (type === 'Earn' && ) || (type === 'Revenue' && ) || (type === 'Invitification' && ) || (type === 'InvoicePaid' && (n.invoice.nostr ? : )) || (type === 'Referral' && ) || (type === 'Streak' && ) || (type === 'Votification' && ) || (type === 'ForwardedVotification' && ) || (type === 'Mention' && ) || (type === 'JobChanged' && ) || (type === 'Reply' && ) || (type === 'SubStatus' && ) || (type === 'FollowActivity' && ) || (type === 'TerritoryPost' && ) } ) } function NotificationLayout ({ children, nid, href, as, fresh }) { const router = useRouter() if (!href) return
{children}
return (
{ if (ignoreClick(e)) return nid && await router.replace({ pathname: router.pathname, query: { ...router.query, nid } }, router.asPath, { ...router.options, shallow: true }) router.push(href, as) }} > {children}
) } const defaultOnClick = n => { const type = n.__typename if (type === 'Earn') { let href = '/rewards/' if (n.minSortTime !== n.sortTime) { href += `${dayMonthYear(new Date(n.minSortTime))}/` } href += dayMonthYear(new Date(n.sortTime)) return { href } } if (type === 'Revenue') return { href: `/~${n.subName}` } if (type === 'SubStatus') return { href: `/~${n.sub.name}` } if (type === 'Invitification') return { href: '/invites' } if (type === 'InvoicePaid') return { href: `/invoices/${n.invoice.id}` } if (type === 'Referral') return { href: '/referrals/month' } if (type === 'Streak') return {} // Votification, Mention, JobChanged, Reply all have item if (!n.item.title) { const rootId = commentSubTreeRootId(n.item) return { href: { pathname: '/items/[id]', query: { id: rootId, commentId: n.item.id } }, as: `/items/${rootId}` } } else { return { href: { pathname: '/items/[id]', query: { id: n.item.id } }, as: `/items/${n.item.id}` } } } function Streak ({ n }) { function blurb (n) { const index = Number(n.id) % Math.min(FOUND_BLURBS.length, LOST_BLURBS.length) if (n.days) { return `After ${numWithUnits(n.days, { abbreviate: false, unitSingular: 'day', unitPlural: 'days' })}, ` + LOST_BLURBS[index] } return FOUND_BLURBS[index] } return (
{n.days ? : }
you {n.days ? 'lost your' : 'found a'} cowboy hat
{blurb(n)}
) } function EarnNotification ({ n }) { const time = n.minSortTime === n.sortTime ? dayMonthYear(new Date(n.minSortTime)) : `${dayMonthYear(new Date(n.minSortTime))} to ${dayMonthYear(new Date(n.sortTime))}` return (
you stacked {numWithUnits(n.earnedSats, { abbreviate: false })} in rewards{time}
{n.sources &&
{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.
click for details
) } function RevenueNotification ({ n }) { return (
you stacked {numWithUnits(n.earnedSats, { abbreviate: false })} in territory revenue{timeSince(new Date(n.sortTime))}
As the founder of territory ~{n.subName}, you receive 50% of the revenue it generates and the other 50% go to rewards.
) } function SubStatus ({ n }) { const dueDate = nextBillingWithGrace(n.sub) return (
{n.sub.status === 'ACTIVE' ? 'your territory is active again' : (n.sub.status === 'GRACE' ? <>your territory payment for ~{n.sub.name} is due or your territory will be archived in : <>your territory ~{n.sub.name} has been archived)} click to visit territory and pay
) } function Invitification ({ n }) { return ( <> your invite has been redeemed by {numWithUnits(n.invite.invitees.length, { abbreviate: false, unitSingular: 'stacker', unitPlural: 'stackers' })}
= n.invite.limit) } />
) } function NostrZap ({ n }) { const { nostr } = n.invoice const { npub, content, note } = nostrZapDetails(nostr) return ( <>
{numWithUnits(n.earnedSats)} zap from {npub.slice(0, 10)}... on {note ? ( {note.slice(0, 12)}... ) : 'nostr'} {timeSince(new Date(n.sortTime))} {content && {content}}
) } function InvoicePaid ({ n }) { let payerSig if (n.invoice.lud18Data) { const { name, identifier, email, pubkey } = n.invoice.lud18Data const id = identifier || email || pubkey payerSig = '- ' if (name) { payerSig += name if (id) payerSig += ' \\ ' } if (id) payerSig += id } return (
{numWithUnits(n.earnedSats, { abbreviate: false, unitSingular: 'sat was', unitPlural: 'sats were' })} deposited in your account {timeSince(new Date(n.sortTime))} {n.invoice.comment && {n.invoice.comment} {payerSig} }
) } function Referral ({ n }) { return ( someone joined via one of your referral links {timeSince(new Date(n.sortTime))} ) } function Votification ({ n }) { let forwardedSats = 0 let ForwardedUsers = null if (n.item.forwards?.length) { forwardedSats = Math.floor(n.earnedSats * n.item.forwards.map(fwd => fwd.pct).reduce((sum, cur) => sum + cur) / 100) ForwardedUsers = () => n.item.forwards.map((fwd, i) => @{fwd.user.name} {i !== n.item.forwards.length - 1 && ' '} ) } return ( <> your {n.item.title ? 'post' : 'reply'} stacked {numWithUnits(n.earnedSats, { abbreviate: false })} {n.item.forwards?.length > 0 && <> {' '}and forwarded {numWithUnits(forwardedSats, { abbreviate: false })} to{' '} }
{n.item.title ? : (
)}
) } function ForwardedVotification ({ n }) { return ( <> you were forwarded {numWithUnits(n.earnedSats, { abbreviate: false })} from
{n.item.title ? : (
)}
) } function Mention ({ n }) { return ( <> you were mentioned in
{n.item.title ? : (
)}
) } function JobChanged ({ n }) { return ( <> {n.item.status === 'ACTIVE' ? 'your job is active again' : (n.item.status === 'NOSATS' ? 'your job promotion ran out of sats' : 'your job has been stopped')} ) } function Reply ({ n }) { return (
{n.item.title ? : (
)}
) } function FollowActivity ({ n }) { return ( <> a stacker you subscribe to {n.item.parentId ? 'commented' : 'posted'} {n.item.title ?
: (
)} ) } function TerritoryPost ({ n }) { return ( <> new post in ~{n.item.sub.name}
) } export function NotificationAlert () { const [showAlert, setShowAlert] = useState(false) const [hasSubscription, setHasSubscription] = useState(false) const [error, setError] = useState(null) const [supported, setSupported] = useState(false) const sw = useServiceWorker() useEffect(() => { const isSupported = sw.support.serviceWorker && sw.support.pushManager && sw.support.notification if (isSupported) { const isDefaultPermission = sw.permission.notification === 'default' setShowAlert(isDefaultPermission && !window.localStorage.getItem('hideNotifyPrompt')) sw.registration?.pushManager.getSubscription().then(subscription => setHasSubscription(!!subscription)) setSupported(true) } }, [sw]) const close = () => { window.localStorage.setItem('hideNotifyPrompt', 'yep') setShowAlert(false) } return ( error ? ( setError(null)}> {error.toString()} ) : showAlert ? ( Enable push notifications? ) : (
push notifications} groupClassName={`${styles.subFormGroup} mb-1 me-sm-3 me-0`} inline checked={hasSubscription} handleChange={async () => { await sw.togglePushSubscription().catch(setError) }} /> ) ) } const nid = n => n.__typename + n.id + n.sortTime export default function Notifications ({ ssrData }) { const { data, fetchMore } = useQuery(NOTIFICATIONS) const router = useRouter() const dat = useData(data, ssrData) const { notifications: { notifications, lastChecked, cursor } } = useMemo(() => { return dat || { notifications: {} } }, [dat]) useEffect(() => { if (lastChecked && !router?.query?.checkedAt) { router.replace({ pathname: router.pathname, query: { ...router.query, nodata: true, // make sure nodata is set so we don't fetch on back/forward checkedAt: lastChecked } }, router.asPath, { ...router.options, shallow: true }) } }, [router, lastChecked]) if (!dat) return return ( <> {notifications.map(n => new Date(router?.query?.checkedAt)} />)} ) } function CommentsFlatSkeleton () { const comments = new Array(21).fill(null) return (
{comments.map((_, i) => ( ))}
) }