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 { COMMENT_DEPTH_LIMIT, 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'
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' && )
}
)
}
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 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) % 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{' '}
>}
>
)
}
function ForwardedVotification ({ n }) {
return (
<>
you were forwarded {numWithUnits(n.earnedSats, { abbreviate: false })} from
>
)
}
function Mention ({ n }) {
return (
<>
you were mentioned in
>
)
}
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 (
)
}
function FollowActivity ({ n }) {
return (
<>
a stacker you subscribe to {n.item.parentId ? 'commented' : 'posted'}
{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?
{
await sw.requestNotificationPermission()
.then(close)
.catch(setError)
}}
>Yes
No
)
: (
)
)
}
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) => (
))}
)
}