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:
ekzyis 2023-06-12 20:03:44 +02:00 committed by GitHub
parent 876b3e0fdd
commit 069417d130
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 103 additions and 24 deletions

View File

@ -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}>

View File

@ -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>
}

View File

@ -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;
}

View File

@ -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'