Notification badges (#595)
* First pass of implementing Badging API for notifications * Only show app badge when driven from push notifications * Display number of unread push notifications instead of just an empty badge Clear badge via postMessage when notifications page is loaded * de-dupe some code, update badge counter on each notification click * remove ids, track open note count instead * restore optional chaining * ensure note count doesn't go below 0, and fix event.waitUntil error when clearing badge * incorporate PR feedback
This commit is contained in:
parent
3a56782572
commit
522c821c89
|
@ -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 (
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 (
|
||||
|
|
|
@ -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)
|
||||
})())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue