2023-06-01 00:28:33 +02:00
import { useState , useCallback , useEffect , useContext , createContext } from 'react'
2021-09-06 17:36:08 -05:00
import { useQuery } from '@apollo/client'
2021-08-17 13:15:24 -05:00
import Comment , { CommentSkeleton } from './comment'
2022-07-21 17:55:05 -05:00
import Item from './item'
import ItemJob from './item-job'
2021-08-17 13:15:24 -05:00
import { NOTIFICATIONS } from '../fragments/notifications'
2021-08-17 18:59:22 -05:00
import { useRouter } from 'next/router'
2021-09-30 10:46:58 -05:00
import MoreFooter from './more-footer'
2022-01-19 15:02:38 -06:00
import Invite from './invite'
2022-01-20 13:03:48 -06:00
import { ignoreClick } from '../lib/clicks'
2022-03-31 11:49:35 -05:00
import { timeSince } from '../lib/time'
2022-03-17 15:13:19 -05:00
import Link from 'next/link'
2022-03-23 13:54:39 -05:00
import Check from '../svgs/check-double-line.svg'
2022-03-23 16:45:36 -05:00
import HandCoin from '../svgs/hand-coin-fill.svg'
2022-05-17 17:09:15 -05:00
import { COMMENT _DEPTH _LIMIT } from '../lib/constants'
2023-02-01 08:44:35 -06:00
import CowboyHatIcon from '../svgs/cowboy.svg'
import BaldIcon from '../svgs/bald.svg'
2023-05-06 16:51:17 -05:00
import { RootProvider } from './root'
2023-06-01 00:28:33 +02:00
import { useMe } from './me'
2021-08-17 13:15:24 -05:00
2021-09-06 17:36:08 -05:00
function Notification ( { n } ) {
2023-06-01 02:51:30 +02: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 } / >
}
2023-06-01 13:22:39 -05:00
console . error ( '__typename not supported:' , n . _ _typename )
2023-06-01 02:51:30 +02:00
return null
}
2022-01-20 13:03:48 -06:00
2023-06-01 13:22:39 -05:00
function NotificationLayout ( { children , onClick } ) {
2023-06-01 02:51:30 +02:00
return (
2023-06-01 13:22:39 -05:00
< div
className = 'clickToContext' onClick = { ( e ) => {
if ( ignoreClick ( e ) ) return
onClick ? . ( e )
} }
>
2023-06-01 02:51:30 +02:00
{ children }
2023-02-01 08:44:35 -06:00
< / d i v >
)
}
2023-06-01 13:22:39 -05:00
const defaultOnClick = ( n , router ) => ( ) => {
2023-06-01 02:51:30 +02:00
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 08:44:35 -06: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-19 19:13:32 -05:00
< / d i v >
)
}
2023-06-01 13:22:39 -05:00
function EarnNotification ( { n } ) {
2023-06-01 02:51:30 +02:00
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 >
2023-06-01 13:22:39 -05:00
)
2023-06-01 02:51:30 +02:00
}
2023-06-01 13:22:39 -05:00
function Invitification ( { n } ) {
2023-06-01 02:51:30 +02:00
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 >
)
}
2023-06-01 13:22:39 -05:00
function InvoicePaid ( { n } ) {
2023-06-01 02:51:30 +02:00
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 >
)
}
2023-06-01 13:22:39 -05:00
function Referral ( { n } ) {
2023-06-01 02:51:30 +02:00
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 >
)
}
2023-06-01 13:22:39 -05:00
function Votification ( { n } ) {
const router = useRouter ( )
2023-06-01 02:51:30 +02:00
return (
2023-06-01 13:22:39 -05:00
< NotificationLayout onClick = { defaultOnClick ( n , router ) } >
2023-06-01 02:51:30 +02:00
< 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 >
2023-06-01 13:22:39 -05:00
) }
2023-06-01 02:51:30 +02:00
< / d i v >
< / N o t i f i c a t i o n L a y o u t >
)
}
2023-06-01 13:22:39 -05:00
function Mention ( { n } ) {
const router = useRouter ( )
2023-06-01 02:51:30 +02:00
return (
2023-06-01 13:22:39 -05:00
< NotificationLayout onClick = { defaultOnClick ( n , router ) } >
2023-06-01 02:51:30 +02:00
< small className = 'font-weight-bold text-info ml-2' >
you were mentioned in
< / s m a l l >
< div >
2023-06-01 13:22:39 -05:00
{ 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 > ) }
2023-06-01 02:51:30 +02:00
< / d i v >
< / N o t i f i c a t i o n L a y o u t >
)
}
2023-06-01 13:22:39 -05:00
function JobChanged ( { n } ) {
const router = useRouter ( )
2023-06-01 02:51:30 +02:00
return (
2023-06-01 13:22:39 -05:00
< NotificationLayout onClick = { defaultOnClick ( n , router ) } >
2023-06-01 02:51:30 +02:00
< 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 >
)
}
2023-06-01 13:22:39 -05:00
function Reply ( { n } ) {
const router = useRouter ( )
2023-06-01 02:51:30 +02:00
return (
2023-06-01 13:22:39 -05:00
< NotificationLayout onClick = { defaultOnClick ( n , router ) } rootText = 'replying on:' >
< div className = 'py-2' >
2023-06-01 02:51:30 +02:00
{ 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 >
2023-06-01 13:22:39 -05:00
) }
2023-06-01 02:51:30 +02:00
< / d i v >
< / N o t i f i c a t i o n L a y o u t >
)
}
2022-04-24 11:13:07 -05:00
export default function Notifications ( { notifications , earn , cursor , lastChecked , variables } ) {
2021-10-26 15:49:37 -05:00
const { data , fetchMore } = useQuery ( NOTIFICATIONS , { variables } )
2021-08-19 19:13:32 -05:00
2021-09-30 10:46:58 -05:00
if ( data ) {
2022-04-24 11:13:07 -05:00
( { notifications : { notifications , earn , cursor } } = data )
2021-09-30 10:46:58 -05:00
}
2021-08-19 19:13:32 -05:00
const [ fresh , old ] =
notifications . reduce ( ( result , n ) => {
result [ new Date ( n . sortTime ) . getTime ( ) > lastChecked ? 0 : 1 ] . push ( n )
return result
} ,
[ [ ] , [ ] ] )
2021-08-17 18:59:22 -05:00
2021-08-17 13:15:24 -05:00
return (
< >
2021-08-17 18:07:52 -05:00
{ /* XXX we shouldn't use the index but we don't have a unique id in this union yet */ }
2021-11-04 14:22:03 -04:00
< div className = 'fresh' >
2022-04-24 11:13:07 -05:00
{ earn && < Notification n = { earn } key = 'earn' / > }
2021-08-19 19:13:32 -05: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 13:15:24 -05:00
) ) }
2021-09-30 10:46:58 -05:00
< MoreFooter cursor = { cursor } fetchMore = { fetchMore } Skeleton = { CommentsFlatSkeleton } / >
2021-08-17 13:15:24 -05:00
< / >
)
}
function CommentsFlatSkeleton ( ) {
const comments = new Array ( 21 ) . fill ( null )
return (
< div > { comments . map ( ( _ , i ) => (
< CommentSkeleton key = { i } skeletonChildren = { 0 } / >
) ) }
< / d i v >
)
}
2023-06-01 00:28:33 +02: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 ( )
2023-06-01 14:41:20 -05:00
const router = useRouter ( )
2023-06-01 00:28:33 +02:00
const show _ = ( title , options ) => {
const icon = '/android-chrome-24x24.png'
2023-06-01 14:41:20 -05:00
return new window . Notification ( title , { icon , ... options } )
2023-06-01 00:28:33 +02:00
}
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' )
2023-06-01 14:41:20 -05:00
show _ ( 'Stacker News notifications enabled' )
2023-06-01 00:28:33 +02:00
}
} )
} , [ isDefaultPermission ] )
useEffect ( ( ) => {
2023-06-01 14:41:20 -05:00
if ( ! me || ! isSupported || ! isDefaultPermission || router . pathname !== '/notifications' ) return
2023-06-01 00:28:33 +02:00
requestPermission ( )
2023-06-01 14:41:20 -05:00
} , [ router ? . pathname ] )
2023-06-01 00:28:33 +02:00
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 )
}