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 { 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 { COMMENT_DEPTH_LIMIT } 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' function Notification ({ n, fresh }) { const type = n.__typename return ( { (type === 'Earn' && ) || (type === 'Invitification' && ) || (type === 'InvoicePaid' && (n.invoice.nostr ? : )) || (type === 'Referral' && ) || (type === 'Streak' && ) || (type === 'Votification' && ) || (type === 'Mention' && ) || (type === 'JobChanged' && ) || (type === 'Reply' && ) } ) } 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') return { href: `/rewards/${new Date(n.sortTime).toISOString().slice(0, 10)}` } 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 path = n.item.path.split('.') if (path.length > COMMENT_DEPTH_LIMIT + 1) { const rootId = path.slice(-(COMMENT_DEPTH_LIMIT + 1))[0] 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.root.id, commentId: n.item.id } }, as: `/items/${n.item.root.id}` } } } 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) % 6 const FOUND_BLURBS = [ 'The harsh frontier is no place for the unprepared. This hat will protect you from the sun, dust, and other elements Mother Nature throws your way.', 'A cowboy is nothing without a cowboy hat. Take good care of it, and it will protect you from the sun, dust, and other elements on your journey.', "This is not just a hat, it's a matter of survival. Take care of this essential tool, and it will shield you from the scorching sun and the elements.", "A cowboy hat isn't just a fashion statement. It's your last defense against the unforgiving elements of the Wild West. Hang onto it tight.", "A good cowboy hat is worth its weight in gold, shielding you from the sun, wind, and dust of the western frontier. Don't lose it.", 'Your cowboy hat is the key to your survival in the wild west. Treat it with respect and it will protect you from the elements.' ] const LOST_BLURBS = [ 'your cowboy hat was taken by the wind storm that blew in from the west. No worries, a true cowboy always finds another hat.', "you left your trusty cowboy hat in the saloon before leaving town. You'll need a replacement for the long journey west.", 'you lost your cowboy hat in a wild shoot-out on the outskirts of town. Tough luck, tIme to start searching for another one.', 'you ran out of food and had to trade your hat for supplies. Better start looking for another hat.', "your hat was stolen by a mischievous prairie dog. You won't catch the dog, but you can always find another hat.", 'you lost your hat while crossing the river on your journey west. Maybe you can find a replacement hat in the next town.' ] 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 }) { return (
you stacked {numWithUnits(n.earnedSats, { abbreviate: false })} in rewards{timeSince(new Date(n.sortTime))}
{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.
) } 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 }) { return (
{numWithUnits(n.earnedSats, { abbreviate: false })} were deposited in your account {timeSince(new Date(n.sortTime))}
) } function Referral ({ n }) { return ( someone joined via one of your referral links {timeSince(new Date(n.sortTime))} ) } function Votification ({ n }) { return ( <> 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 ? : (
)}
) } 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 ? : (
)}
) } 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) => ( ))}
) }