2023-05-31 22:28:33 +00:00
import { useState , useCallback , useEffect , useContext , createContext } from 'react'
2021-09-06 22:36:08 +00:00
import { useQuery } from '@apollo/client'
2021-08-17 18:15:24 +00:00
import Comment , { CommentSkeleton } from './comment'
2022-07-21 22:55:05 +00:00
import Item from './item'
import ItemJob from './item-job'
2021-08-17 18:15:24 +00:00
import { NOTIFICATIONS } from '../fragments/notifications'
2021-08-17 23:59:22 +00:00
import { useRouter } from 'next/router'
2021-09-30 15:46:58 +00:00
import MoreFooter from './more-footer'
2022-01-19 21:02:38 +00:00
import Invite from './invite'
2022-01-20 19:03:48 +00:00
import { ignoreClick } from '../lib/clicks'
2022-03-31 16:49:35 +00:00
import { timeSince } from '../lib/time'
2022-03-17 20:13:19 +00:00
import Link from 'next/link'
2022-03-23 18:54:39 +00:00
import Check from '../svgs/check-double-line.svg'
2022-03-23 21:45:36 +00:00
import HandCoin from '../svgs/hand-coin-fill.svg'
2022-05-17 22:09:15 +00:00
import { COMMENT _DEPTH _LIMIT } from '../lib/constants'
2023-02-01 14:44:35 +00:00
import CowboyHatIcon from '../svgs/cowboy.svg'
import BaldIcon from '../svgs/bald.svg'
2023-05-06 21:51:17 +00:00
import { RootProvider } from './root'
2023-05-31 22:28:33 +00:00
import { useMe } from './me'
2021-08-17 18:15:24 +00:00
2021-09-06 22:36:08 +00:00
function Notification ( { n } ) {
2023-06-01 00:51:30 +00:00
switch ( n . _ _typename ) {
case 'Earn' : return < EarnNotification n = { n } / >
case 'Invitification' : return < Invitification n = { n } / >
case 'InvoicePaid' : return < InvoicePaid n = { n } / >
case 'Referral' : return < Referral n = { n } / >
case 'Streak' : return < Streak n = { n } / >
case 'Votification' : return < Votification n = { n } / >
case 'Mention' : return < Mention n = { n } / >
case 'JobChanged' : return < JobChanged n = { n } / >
case 'Reply' : return < Reply n = { n } / >
}
console . error ( "__typename not supported:" , n . _ _typename )
return null
}
2022-01-20 19:03:48 +00:00
2023-06-01 00:51:30 +00:00
function NotificationLayout ( { children , onClick } ) {
return (
< div className = 'clickToContext' onClick = { ( e ) => {
if ( ignoreClick ( e ) ) return
onClick ? . ( e )
} } >
{ children }
2023-02-01 14:44:35 +00:00
< / d i v >
)
}
2023-06-01 00:51:30 +00:00
const defaultOnClick = ( n ) => ( ) => {
if ( ! n . item . title ) {
if ( n . item . path . split ( '.' ) . length > COMMENT _DEPTH _LIMIT + 1 ) {
router . push ( {
pathname : '/items/[id]' ,
query : { id : n . item . parentId , commentId : n . item . id }
} , ` /items/ ${ n . item . parentId } ` )
} else {
router . push ( {
pathname : '/items/[id]' ,
query : { id : n . item . root . id , commentId : n . item . id }
} , ` /items/ ${ n . item . root . id } ` )
}
} else {
router . push ( {
pathname : '/items/[id]' ,
query : { id : n . item . id }
} , ` /items/ ${ n . item . id } ` )
}
}
2023-02-01 14:44:35 +00:00
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 ${ n . days } days, ` + LOST _BLURBS [ index ]
}
return FOUND _BLURBS [ index ]
}
return (
< div className = 'd-flex font-weight-bold ml-2 py-1' >
< div style = { { fontSize : '2rem' } } > { n . days ? < BaldIcon className = 'fill-grey' height = { 40 } width = { 40 } / > : < CowboyHatIcon className = 'fill-grey' height = { 40 } width = { 40 } / > } < / d i v >
< div className = 'ml-1 p-1' >
you { n . days ? 'lost your' : 'found a' } cowboy hat
< div > < small style = { { lineHeight : '140%' , display : 'inline-block' } } > { blurb ( n ) } < / s m a l l > < / d i v >
< / d i v >
2021-08-20 00:13:32 +00:00
< / d i v >
)
}
2023-06-01 00:51:30 +00:00
function EarnNotification ( { n } ) {
return (
< NotificationLayout >
< div className = 'd-flex' >
< HandCoin className = 'align-self-center fill-boost mx-1' width = { 24 } height = { 24 } style = { { flex : '0 0 24px' , transform : 'rotateY(180deg)' } } / >
< div className = 'ml-2' >
< div className = 'font-weight-bold text-boost' >
you stacked { n . earnedSats } sats in rewards < small className = 'text-muted ml-1' > { timeSince ( new Date ( n . sortTime ) ) } < / s m a l l >
< / d i v >
{ n . sources &&
< div style = { { fontSize : '80%' , color : 'var(--theme-grey)' } } >
{ n . sources . posts > 0 && < span > { n . sources . posts } sats for top posts < / s p a n > }
{ n . sources . comments > 0 && < span > { n . sources . posts > 0 && ' \\ ' } { n . sources . comments } sats for top comments < / s p a n > }
{ n . sources . tips > 0 && < span > { ( n . sources . comments > 0 || n . sources . posts > 0 ) && ' \\ ' } { n . sources . tips } sats for tipping top content early < / s p a n > }
< / d i v > }
< div className = 'pb-1' style = { { lineHeight : '140%' } } >
SN distributes the sats it earns back to its best users daily . These sats come from < Link href = '/~jobs' passHref > < a > jobs < / a > < / L i n k > , b o o s t s , p o s t i n g f e e s , a n d d o n a t i o n s . Y o u c a n s e e t h e d a i l y r e w a r d s p o o l a n d m a k e a d o n a t i o n < L i n k h r e f = ' / r e w a r d s ' p a s s H r e f > < a > h e r e < / a > < / L i n k > .
< / d i v >
< / d i v >
< / d i v >
< / N o t i f i c a t i o n L a y o u t >
) ;
}
function Invitification ( { n } ) {
const router = useRouter ( )
return (
< NotificationLayout onClick = { ( ) => router . push ( '/invites' ) } >
< small className = 'font-weight-bold text-secondary ml-2' >
your invite has been redeemed by { n . invite . invitees . length } users
< / s m a l l >
< div className = 'ml-4 mr-2 mt-1' >
< Invite
invite = { n . invite } active = {
! n . invite . revoked &&
! ( n . invite . limit && n . invite . invitees . length >= n . invite . limit )
}
/ >
< / d i v >
< / N o t i f i c a t i o n L a y o u t >
)
}
function InvoicePaid ( { n } ) {
const router = useRouter ( )
return (
< NotificationLayout onClick = { ( ) => router . push ( ` /invoices/ ${ n . invoice . id } ` ) } >
< div className = 'font-weight-bold text-info ml-2 py-1' >
< Check className = 'fill-info mr-1' / > { n . earnedSats } sats were deposited in your account
< small className = 'text-muted ml-1' > { timeSince ( new Date ( n . sortTime ) ) } < / s m a l l >
< / d i v >
< / N o t i f i c a t i o n L a y o u t >
)
}
function Referral ( { n } ) {
return (
< NotificationLayout >
< small className = 'font-weight-bold text-secondary ml-2' >
someone joined via one of your < Link href = '/referrals/month' passHref > < a className = 'text-reset' > referral links < / a > < / L i n k >
< small className = 'text-muted ml-1' > { timeSince ( new Date ( n . sortTime ) ) } < / s m a l l >
< / s m a l l >
< / N o t i f i c a t i o n L a y o u t >
)
}
function Votification ( { n } ) {
return (
< NotificationLayout onClick = { defaultOnClick ( n ) } >
< small className = 'font-weight-bold text-success ml-2' >
your { n . item . title ? 'post' : 'reply' } { n . item . fwdUser ? 'forwarded' : 'stacked' } { n . earnedSats } sats { n . item . fwdUser && ` to @ ${ n . item . fwdUser . name } ` }
< / s m a l l >
< div >
{ n . item . title
? < Item item = { n . item } / >
: (
< div className = 'pb-2' >
< RootProvider root = { n . item . root } >
< Comment item = { n . item } noReply includeParent clickToContext / >
< / R o o t P r o v i d e r >
< / d i v >
)
}
< / d i v >
< / N o t i f i c a t i o n L a y o u t >
)
}
function Mention ( { n } ) {
return (
< NotificationLayout onClick = { defaultOnClick ( n ) } >
< small className = 'font-weight-bold text-info ml-2' >
you were mentioned in
< / s m a l l >
< div >
{ n . item . title
? < Item item = { n . item } / >
: (
< div className = 'pb-2' >
< RootProvider root = { n . item . root } >
< Comment item = { n . item } noReply includeParent rootText = { n . _ _typename === 'Reply' ? 'replying on:' : undefined } clickToContext / >
< / R o o t P r o v i d e r >
< / d i v > )
}
< / d i v >
< / N o t i f i c a t i o n L a y o u t >
)
}
function JobChanged ( { n } ) {
return (
< NotificationLayout onClick = { defaultOnClick ( n ) } >
< small className = { ` font-weight-bold text- ${ n . item . status === 'ACTIVE' ? 'success' : 'boost' } ml-1 ` } >
{ 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' ) }
< / s m a l l >
< ItemJob item = { n . item } / >
< / N o t i f i c a t i o n L a y o u t >
)
}
function Reply ( { n } ) {
return (
< NotificationLayout onClick = { defaultOnClick ( n ) } rootText = 'replying on:' >
< div className = "py-2" >
{ n . item . title
? < Item item = { n . item } / >
: (
< div className = 'pb-2' >
< RootProvider root = { n . item . root } >
< Comment item = { n . item } noReply includeParent clickToContext rootText = 'replying on:' / >
< / R o o t P r o v i d e r >
< / d i v >
)
}
< / d i v >
< / N o t i f i c a t i o n L a y o u t >
)
}
2022-04-24 16:13:07 +00:00
export default function Notifications ( { notifications , earn , cursor , lastChecked , variables } ) {
2021-10-26 20:49:37 +00:00
const { data , fetchMore } = useQuery ( NOTIFICATIONS , { variables } )
2021-08-20 00:13:32 +00:00
2021-09-30 15:46:58 +00:00
if ( data ) {
2022-04-24 16:13:07 +00:00
( { notifications : { notifications , earn , cursor } } = data )
2021-09-30 15:46:58 +00:00
}
2021-08-20 00:13:32 +00:00
const [ fresh , old ] =
notifications . reduce ( ( result , n ) => {
result [ new Date ( n . sortTime ) . getTime ( ) > lastChecked ? 0 : 1 ] . push ( n )
return result
} ,
[ [ ] , [ ] ] )
2021-08-17 23:59:22 +00:00
2021-08-17 18:15:24 +00:00
return (
< >
2021-08-17 23:07:52 +00:00
{ /* XXX we shouldn't use the index but we don't have a unique id in this union yet */ }
2021-11-04 18:22:03 +00:00
< div className = 'fresh' >
2022-04-24 16:13:07 +00:00
{ earn && < Notification n = { earn } key = 'earn' / > }
2021-08-20 00:13:32 +00:00
{ fresh . map ( ( n , i ) => (
< Notification n = { n } key = { i } / >
) ) }
< / d i v >
{ old . map ( ( n , i ) => (
< Notification n = { n } key = { i } / >
2021-08-17 18:15:24 +00:00
) ) }
2021-09-30 15:46:58 +00:00
< MoreFooter cursor = { cursor } fetchMore = { fetchMore } Skeleton = { CommentsFlatSkeleton } / >
2021-08-17 18:15:24 +00:00
< / >
)
}
function CommentsFlatSkeleton ( ) {
const comments = new Array ( 21 ) . fill ( null )
return (
< div > { comments . map ( ( _ , i ) => (
< CommentSkeleton key = { i } skeletonChildren = { 0 } / >
) ) }
< / d i v >
)
}
2023-05-31 22:28:33 +00:00
const NotificationContext = createContext ( { } )
export const NotificationProvider = ( { children } ) => {
const isBrowser = typeof window !== 'undefined'
const [ isSupported ] = useState ( isBrowser ? 'Notification' in window : false )
const [ isDefaultPermission , setIsDefaultPermission ] = useState ( isSupported ? window . Notification . permission === 'default' : undefined )
const [ isGranted , setIsGranted ] = useState ( isSupported ? window . Notification . permission === 'granted' : undefined )
const me = useMe ( )
const show _ = ( title , options ) => {
const icon = '/android-chrome-24x24.png'
new window . Notification ( title , { icon , ... options } )
}
const show = useCallback ( ( ... args ) => {
if ( ! isGranted ) return
show _ ( ... args )
} , [ isGranted ] )
const requestPermission = useCallback ( ( ) => {
window . Notification . requestPermission ( ) . then ( result => {
setIsDefaultPermission ( window . Notification . permission === 'default' )
if ( result === 'granted' ) {
setIsGranted ( result === 'granted' )
show _ ( 'you have enabled notifications' )
}
} )
} , [ isDefaultPermission ] )
useEffect ( ( ) => {
if ( ! me || ! isSupported || ! isDefaultPermission ) return
requestPermission ( )
} , [ ] )
const ctx = { isBrowser , isSupported , isDefaultPermission , isGranted , show }
return < NotificationContext . Provider value = { ctx } > { children } < / N o t i f i c a t i o n C o n t e x t . P r o v i d e r >
}
export function useNotification ( ) {
return useContext ( NotificationContext )
}