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 AnonIcon from '../svgs/spy-fill.svg'
|
||||||
import Hat from './hat'
|
import Hat from './hat'
|
||||||
import HiddenWalletSummary from './hidden-wallet-summary'
|
import HiddenWalletSummary from './hidden-wallet-summary'
|
||||||
|
import { clearNotifications } from '../lib/badge'
|
||||||
|
|
||||||
function WalletSummary ({ me }) {
|
function WalletSummary ({ me }) {
|
||||||
if (!me) return null
|
if (!me) return null
|
||||||
|
@ -56,7 +57,12 @@ function NotificationBell () {
|
||||||
? {}
|
? {}
|
||||||
: {
|
: {
|
||||||
pollInterval: 30000,
|
pollInterval: 30000,
|
||||||
nextFetchPolicy: 'cache-and-network'
|
nextFetchPolicy: 'cache-and-network',
|
||||||
|
onCompleted: ({ hasNewNotes }) => {
|
||||||
|
if (!hasNewNotes) {
|
||||||
|
clearNotifications()
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
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 Notifications, { NotificationAlert } from '../components/notifications'
|
||||||
import { HAS_NOTIFICATIONS, NOTIFICATIONS } from '../fragments/notifications'
|
import { HAS_NOTIFICATIONS, NOTIFICATIONS } from '../fragments/notifications'
|
||||||
import { useApolloClient } from '@apollo/client'
|
import { useApolloClient } from '@apollo/client'
|
||||||
|
import { clearNotifications } from '../lib/badge'
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps({ query: NOTIFICATIONS, authRequired: true })
|
export const getServerSideProps = getGetServerSideProps({ query: NOTIFICATIONS, authRequired: true })
|
||||||
|
|
||||||
|
@ -17,6 +18,7 @@ export default function NotificationPage ({ ssrData }) {
|
||||||
hasNewNotes: false
|
hasNewNotes: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
clearNotifications()
|
||||||
}, [ssrData])
|
}, [ssrData])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import ServiceWorkerStorage from 'serviceworker-storage'
|
import ServiceWorkerStorage from 'serviceworker-storage'
|
||||||
import { numWithUnits } from '../lib/format'
|
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
|
// we store existing push subscriptions to keep them in sync with server
|
||||||
const storage = new ServiceWorkerStorage('sw:storage', 1)
|
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
|
// keep track of item ids where we received a MENTION notification already to not show one again
|
||||||
const itemMentions = []
|
const itemMentions = []
|
||||||
|
|
||||||
|
// current push notification count for badge purposes
|
||||||
|
let activeCount = 0
|
||||||
|
|
||||||
export function onPush (sw) {
|
export function onPush (sw) {
|
||||||
return async (event) => {
|
return async (event) => {
|
||||||
const payload = event.data?.json()
|
const payload = event.data?.json()
|
||||||
|
@ -20,6 +24,7 @@ export function onPush (sw) {
|
||||||
event.waitUntil((async () => {
|
event.waitUntil((async () => {
|
||||||
if (skipNotification(payload)) return
|
if (skipNotification(payload)) return
|
||||||
if (immediatelyShowNotification(payload)) {
|
if (immediatelyShowNotification(payload)) {
|
||||||
|
setAppBadge(sw, ++activeCount)
|
||||||
return sw.registration.showNotification(payload.title, payload.options)
|
return sw.registration.showNotification(payload.title, payload.options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,6 +44,7 @@ export function onPush (sw) {
|
||||||
|
|
||||||
if (notifications.length === 0) {
|
if (notifications.length === 0) {
|
||||||
// incoming notification is first notification with this tag
|
// incoming notification is first notification with this tag
|
||||||
|
setAppBadge(sw, ++activeCount)
|
||||||
return sw.registration.showNotification(payload.title, payload.options)
|
return sw.registration.showNotification(payload.title, payload.options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,6 +104,12 @@ export function onNotificationClick (sw) {
|
||||||
if (url) {
|
if (url) {
|
||||||
event.waitUntil(sw.clients.openWindow(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()
|
event.notification.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -176,5 +188,18 @@ export function onMessage (sw) {
|
||||||
if (event.data.action === 'DELETE_SUBSCRIPTION') {
|
if (event.data.action === 'DELETE_SUBSCRIPTION') {
|
||||||
return event.waitUntil(storage.removeItem('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