diff --git a/components/header.js b/components/header.js index f92bb617..255a04ef 100644 --- a/components/header.js +++ b/components/header.js @@ -25,6 +25,7 @@ import { HAS_NOTIFICATIONS } from '../fragments/notifications' import AnonIcon from '../svgs/spy-fill.svg' import Hat from './hat' import HiddenWalletSummary from './hidden-wallet-summary' +import { clearNotifications } from '../lib/badge' function WalletSummary ({ me }) { if (!me) return null @@ -56,7 +57,12 @@ function NotificationBell () { ? {} : { pollInterval: 30000, - nextFetchPolicy: 'cache-and-network' + nextFetchPolicy: 'cache-and-network', + onCompleted: ({ hasNewNotes }) => { + if (!hasNewNotes) { + clearNotifications() + } + } }) return ( diff --git a/lib/badge.js b/lib/badge.js new file mode 100644 index 00000000..f241af93 --- /dev/null +++ b/lib/badge.js @@ -0,0 +1,33 @@ +export const CLEAR_NOTIFICATIONS = 'CLEAR_NOTIFICATIONS' + +export const clearNotifications = () => navigator.serviceWorker?.controller?.postMessage({ action: CLEAR_NOTIFICATIONS }) + +const badgingApiSupported = (sw = window) => 'setAppBadge' in sw.navigator + +const permissionGranted = async (sw = window, name = 'notifications') => { + let permission + try { + permission = await sw.navigator.permissions.query({ name }) + } catch (err) { + console.error('Failed to check permissions', err) + } + return permission?.state === 'granted' +} + +export const setAppBadge = async (sw = window, count) => { + if (!badgingApiSupported(sw) || !(await permissionGranted(sw))) return + try { + await sw.navigator.setAppBadge(count) + } catch (err) { + console.error('Failed to set app badge', err) + } +} + +export const clearAppBadge = async (sw = window) => { + if (!badgingApiSupported(sw) || !(await permissionGranted(sw))) return + try { + await sw.navigator.clearAppBadge() + } catch (err) { + console.error('Failed to clear app badge', err) + } +} diff --git a/pages/notifications.js b/pages/notifications.js index d685d4e6..6bfcc0f7 100644 --- a/pages/notifications.js +++ b/pages/notifications.js @@ -4,6 +4,7 @@ import Layout from '../components/layout' import Notifications, { NotificationAlert } from '../components/notifications' import { HAS_NOTIFICATIONS, NOTIFICATIONS } from '../fragments/notifications' import { useApolloClient } from '@apollo/client' +import { clearNotifications } from '../lib/badge' export const getServerSideProps = getGetServerSideProps({ query: NOTIFICATIONS, authRequired: true }) @@ -17,6 +18,7 @@ export default function NotificationPage ({ ssrData }) { hasNewNotes: false } }) + clearNotifications() }, [ssrData]) return ( diff --git a/sw/eventListener.js b/sw/eventListener.js index e7b71a46..c8bfa22e 100644 --- a/sw/eventListener.js +++ b/sw/eventListener.js @@ -1,5 +1,6 @@ import ServiceWorkerStorage from 'serviceworker-storage' import { numWithUnits } from '../lib/format' +import { CLEAR_NOTIFICATIONS, clearAppBadge, setAppBadge } from '../lib/badge' // we store existing push subscriptions to keep them in sync with server const storage = new ServiceWorkerStorage('sw:storage', 1) @@ -12,6 +13,9 @@ let actionChannelPort // keep track of item ids where we received a MENTION notification already to not show one again const itemMentions = [] +// current push notification count for badge purposes +let activeCount = 0 + export function onPush (sw) { return async (event) => { const payload = event.data?.json() @@ -20,6 +24,7 @@ export function onPush (sw) { event.waitUntil((async () => { if (skipNotification(payload)) return if (immediatelyShowNotification(payload)) { + setAppBadge(sw, ++activeCount) return sw.registration.showNotification(payload.title, payload.options) } @@ -39,6 +44,7 @@ export function onPush (sw) { if (notifications.length === 0) { // incoming notification is first notification with this tag + setAppBadge(sw, ++activeCount) return sw.registration.showNotification(payload.title, payload.options) } @@ -98,6 +104,12 @@ export function onNotificationClick (sw) { if (url) { event.waitUntil(sw.clients.openWindow(url)) } + activeCount = Math.max(0, activeCount - 1) + if (activeCount === 0) { + clearAppBadge(sw) + } else { + setAppBadge(sw, activeCount) + } event.notification.close() } } @@ -176,5 +188,18 @@ export function onMessage (sw) { if (event.data.action === 'DELETE_SUBSCRIPTION') { return event.waitUntil(storage.removeItem('subscription')) } + if (event.data.action === CLEAR_NOTIFICATIONS) { + return event.waitUntil((async () => { + let notifications = [] + try { + notifications = await sw.registration.getNotifications() + } catch (err) { + console.error('failed to get notifications') + } + notifications.forEach(notification => notification.close()) + activeCount = 0 + return await clearAppBadge(sw) + })()) + } } }