Enable push notifications in settings (#301)
* Enable push notifications in settings * Fix checkbox still checked after user denied permission The error was related to me thinking that the value prop does anything. It didn't. The value of the checkbox is handled by formik. So the solution was to hook into formik and use the handler which actually changes the value. * Add double opt-in to /notifications * Better styling of alert in /notifications --------- Co-authored-by: ekzyis <ek@stacker.news>
This commit is contained in:
parent
876b3e0fdd
commit
069417d130
|
@ -380,11 +380,11 @@ export function VariableInput ({ label, groupClassName, name, hint, max, min, re
|
|||
)
|
||||
}
|
||||
|
||||
export function Checkbox ({ children, label, groupClassName, hiddenLabel, extra, handleChange, inline, ...props }) {
|
||||
export function Checkbox ({ children, label, groupClassName, hiddenLabel, extra, handleChange, inline, disabled, ...props }) {
|
||||
// React treats radios and checkbox inputs differently other input types, select, and textarea.
|
||||
// Formik does this too! When you specify `type` to useField(), it will
|
||||
// return the correct bag of props for you
|
||||
const [field] = useField({ ...props, type: 'checkbox' })
|
||||
const [field,, helpers] = useField({ ...props, type: 'checkbox' })
|
||||
return (
|
||||
<BootstrapForm.Group className={groupClassName}>
|
||||
{hiddenLabel && <BootstrapForm.Label className='invisible'>{label}</BootstrapForm.Label>}
|
||||
|
@ -394,12 +394,12 @@ export function Checkbox ({ children, label, groupClassName, hiddenLabel, extra,
|
|||
inline={inline}
|
||||
>
|
||||
<BootstrapForm.Check.Input
|
||||
{...field} {...props} type='checkbox' onChange={(e) => {
|
||||
{...field} {...props} disabled={disabled} type='checkbox' onChange={(e) => {
|
||||
field.onChange(e)
|
||||
handleChange && handleChange(e.target.checked)
|
||||
handleChange && handleChange(e.target.checked, helpers.setValue)
|
||||
}}
|
||||
/>
|
||||
<BootstrapForm.Check.Label className='d-flex'>
|
||||
<BootstrapForm.Check.Label className={'d-flex' + (disabled ? ' text-muted' : '')}>
|
||||
<div className='flex-grow-1'>{label}</div>
|
||||
{extra &&
|
||||
<div className={styles.checkboxExtra}>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useCallback, useEffect, useContext, createContext } from 'react'
|
||||
import { useState, useCallback, useContext, useEffect, createContext } from 'react'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import Comment, { CommentSkeleton } from './comment'
|
||||
import Item from './item'
|
||||
|
@ -16,7 +16,8 @@ import { COMMENT_DEPTH_LIMIT } from '../lib/constants'
|
|||
import CowboyHatIcon from '../svgs/cowboy.svg'
|
||||
import BaldIcon from '../svgs/bald.svg'
|
||||
import { RootProvider } from './root'
|
||||
import { useMe } from './me'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
import styles from './notifications.module.css'
|
||||
|
||||
function Notification ({ n }) {
|
||||
switch (n.__typename) {
|
||||
|
@ -251,6 +252,39 @@ function Reply ({ n }) {
|
|||
)
|
||||
}
|
||||
|
||||
function NotificationAlert () {
|
||||
const [showAlert, setShowAlert] = useState(false)
|
||||
const pushNotify = useNotification()
|
||||
|
||||
useEffect(() => {
|
||||
setShowAlert(!localStorage.getItem('hideNotifyPrompt'))
|
||||
}, [])
|
||||
|
||||
const close = () => {
|
||||
localStorage.setItem('hideNotifyPrompt', 'yep')
|
||||
setShowAlert(false)
|
||||
}
|
||||
|
||||
return (
|
||||
showAlert
|
||||
? (
|
||||
<Alert variant='success' className='text-center' dismissible onClose={close}>
|
||||
<span className='align-middle'>Enable push notifications?</span>
|
||||
<button
|
||||
className={`${styles.alertBtn} mx-1`}
|
||||
onClick={() => {
|
||||
pushNotify.requestPermission()
|
||||
close()
|
||||
}}
|
||||
>Yes
|
||||
</button>
|
||||
<button className={`${styles.alertBtn}`} onClick={close}>No</button>
|
||||
</Alert>
|
||||
)
|
||||
: null
|
||||
)
|
||||
}
|
||||
|
||||
export default function Notifications ({ notifications, earn, cursor, lastChecked, variables }) {
|
||||
const { data, fetchMore } = useQuery(NOTIFICATIONS, { variables })
|
||||
|
||||
|
@ -267,6 +301,7 @@ export default function Notifications ({ notifications, earn, cursor, lastChecke
|
|||
|
||||
return (
|
||||
<>
|
||||
<NotificationAlert />
|
||||
{/* XXX we shouldn't use the index but we don't have a unique id in this union yet */}
|
||||
<div className='fresh'>
|
||||
{earn && <Notification n={earn} key='earn' />}
|
||||
|
@ -298,10 +333,18 @@ 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 router = useRouter()
|
||||
const [permission, setPermission_] = useState(
|
||||
isSupported
|
||||
? window.Notification.permission === 'granted'
|
||||
// if permission was granted, we need to check if user has withdrawn permission using the settings
|
||||
// since requestPermission only works once
|
||||
? localStorage.getItem('notify-permission') ?? window.Notification.permission
|
||||
: window.Notification.permission
|
||||
: 'unsupported')
|
||||
const isDefault = permission === 'default'
|
||||
const isGranted = permission === 'granted'
|
||||
const isDenied = permission === 'denied'
|
||||
const isWithdrawn = permission === 'withdrawn'
|
||||
|
||||
const show_ = (title, options) => {
|
||||
const icon = '/android-chrome-24x24.png'
|
||||
|
@ -313,22 +356,22 @@ export const NotificationProvider = ({ children }) => {
|
|||
show_(...args)
|
||||
}, [isGranted])
|
||||
|
||||
const requestPermission = useCallback(() => {
|
||||
const setPermission = useCallback((perm) => {
|
||||
localStorage.setItem('notify-permission', perm)
|
||||
setPermission_(perm)
|
||||
}, [])
|
||||
|
||||
const requestPermission = useCallback((cb) => {
|
||||
window.Notification.requestPermission().then(result => {
|
||||
setIsDefaultPermission(window.Notification.permission === 'default')
|
||||
if (result === 'granted') {
|
||||
setIsGranted(result === 'granted')
|
||||
show_('Stacker News notifications enabled')
|
||||
}
|
||||
setPermission(window.Notification.permission)
|
||||
if (result === 'granted') show_('Stacker News notifications enabled')
|
||||
cb?.(result)
|
||||
})
|
||||
}, [isDefaultPermission])
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!me || !isSupported || !isDefaultPermission || router.pathname !== '/notifications') return
|
||||
requestPermission()
|
||||
}, [router?.pathname])
|
||||
const withdrawPermission = useCallback(() => isGranted ? setPermission('withdrawn') : null, [isGranted])
|
||||
|
||||
const ctx = { isBrowser, isSupported, isDefaultPermission, isGranted, show }
|
||||
const ctx = { isBrowser, isSupported, isDefault, isGranted, isDenied, isWithdrawn, requestPermission, withdrawPermission, show }
|
||||
|
||||
return <NotificationContext.Provider value={ctx}>{children}</NotificationContext.Provider>
|
||||
}
|
||||
|
|
|
@ -11,4 +11,15 @@
|
|||
.fresh {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
border-radius: .4rem;
|
||||
}
|
||||
|
||||
.alertBtn {
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
user-select: none;
|
||||
background-color: transparent;
|
||||
border: 0 solid transparent;
|
||||
font-size: 0.9rem;
|
||||
}
|
|
@ -17,6 +17,7 @@ import { bech32 } from 'bech32'
|
|||
import { NOSTR_MAX_RELAY_NUM, NOSTR_PUBKEY_BECH32 } from '../lib/nostr'
|
||||
import { emailSchema, lastAuthRemovalSchema, settingsSchema } from '../lib/validate'
|
||||
import { SUPPORTED_CURRENCIES } from '../lib/currency'
|
||||
import { useNotification } from '../components/notifications'
|
||||
|
||||
export const getServerSideProps = getGetServerSideProps(SETTINGS)
|
||||
|
||||
|
@ -39,6 +40,7 @@ export default function Settings ({ data: { settings } }) {
|
|||
}
|
||||
}
|
||||
)
|
||||
const pushNotify = useNotification()
|
||||
|
||||
const { data } = useQuery(SETTINGS)
|
||||
if (data) {
|
||||
|
@ -52,6 +54,7 @@ export default function Settings ({ data: { settings } }) {
|
|||
<Form
|
||||
initial={{
|
||||
tipDefault: settings?.tipDefault || 21,
|
||||
pushNotify: pushNotify.isSupported && pushNotify.isGranted,
|
||||
turboTipping: settings?.turboTipping,
|
||||
fiatCurrency: settings?.fiatCurrency || 'USD',
|
||||
noteItemSats: settings?.noteItemSats,
|
||||
|
@ -143,7 +146,29 @@ export default function Settings ({ data: { settings } }) {
|
|||
items={SUPPORTED_CURRENCIES}
|
||||
required
|
||||
/>
|
||||
<div className='form-label'>notify me when ...</div>
|
||||
{
|
||||
pushNotify.isSupported
|
||||
? (
|
||||
<>
|
||||
<div className='form-label'>notify me</div>
|
||||
<Checkbox
|
||||
disabled={pushNotify.isDenied}
|
||||
label='with push notifications'
|
||||
name='pushNotify'
|
||||
groupClassName='mb-0'
|
||||
handleChange={(val, setValue) => {
|
||||
val
|
||||
? pushNotify.requestPermission(result => {
|
||||
if (result === 'denied') setValue(false)
|
||||
})
|
||||
: pushNotify.withdrawPermission()
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
: <div className='form-label'>notify me when ...</div>
|
||||
}
|
||||
<div className='form-label'>when ...</div>
|
||||
<Checkbox
|
||||
label='I stack sats from posts and comments'
|
||||
name='noteItemSats'
|
||||
|
|
Loading…
Reference in New Issue