Fix silent push due to missing tag support in Safari on iOS (#719)

* Merge notifications manually without relying on tag

* Use tag as argument

* Fix title and undefined sats in DEPOSIT push notification

* Remove wrong comment

* Fix wrong var used for tag check

* Also immediately display STREAK notifications

* Close all notifications with same tag manually before

* Fix merge of DEPOSIT notifications

* Remove unused tag from reduce argument

* Remove FIXME(iOS) comment

---------

Co-authored-by: ekzyis <ek@stacker.news>
This commit is contained in:
ekzyis 2023-12-30 01:04:07 +01:00 committed by GitHub
parent 6faec2e7cf
commit 7f512d6adb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 89 additions and 45 deletions

View File

@ -19,72 +19,116 @@ export function onPush (sw) {
if (!payload) return
const { tag } = payload.options
event.waitUntil((async () => {
if (immediatelyShowNotification(payload)) {
if (immediatelyShowNotification(tag)) {
setAppBadge(sw, ++activeCount)
// due to missing proper tag support in Safari on iOS, we can't rely on the tag property to replace notifications.
// see https://bugs.webkit.org/show_bug.cgi?id=258922 for more information
// we therefore fetch all notifications with the same tag (+ manual filter),
// close them and then we display the notification.
const notifications = await sw.registration.getNotifications({ tag })
notifications.filter(({ tag: nTag }) => nTag === tag).forEach(n => n.close())
return await sw.registration.showNotification(payload.title, payload.options)
}
// fetch existing notifications with same tag
const notifications = await sw.registration.getNotifications({ tag })
// since we used a tag filter, there should only be zero or one notification
if (notifications.length > 1) {
const message = `[sw:push] more than one notification with tag ${tag} found`
messageChannelPort?.postMessage({ level: 'error', message })
console.error(message)
return null
}
let notifications = await sw.registration.getNotifications({ tag })
// according to the spec, there should only be zero or one notification since we used a tag filter
// handle zero case here
if (notifications.length === 0) {
// incoming notification is first notification with this tag
setAppBadge(sw, ++activeCount)
return await sw.registration.showNotification(payload.title, payload.options)
}
const currentNotification = notifications[0]
return await mergeAndShowNotification(sw, payload, currentNotification)
// handle unexpected case here
if (notifications.length > 1) {
const message = `[sw:push] more than one notification with tag ${tag} found`
messageChannelPort?.postMessage({ level: 'error', message })
console.error(message)
// due to missing proper tag support in Safari on iOS,
// we only acknowledge this error in our logs and don't bail here anymore
// see https://bugs.webkit.org/show_bug.cgi?id=258922 for more information
const message2 = '[sw:push] skip bail -- merging notifications manually'
messageChannelPort?.postMessage({ level: 'info', message: message2 })
console.log(message2)
// return null
}
// we manually filter notifications by their tag since iOS doesn't properly support tag
// and we're not sure if the built-in tag filter actually filters by tag on iOS
// or if it just returns all currently displayed notifications.
notifications = notifications.filter(({ tag: nTag }) => nTag === tag)
return await mergeAndShowNotification(sw, payload, notifications, tag)
})())
}
}
// if there is no tag or it's a TIP, FORWARDEDTIP or EARN notification
// we don't need to merge notifications and thus the notification should be immediately shown using `showNotification`
const immediatelyShowNotification = ({ options: { tag } }) => !tag || ['TIP', 'FORWARDEDTIP', 'EARN'].includes(tag.split('-')[0])
const immediatelyShowNotification = (tag) => !tag || ['TIP', 'FORWARDEDTIP', 'EARN', 'STREAK'].includes(tag.split('-')[0])
const mergeAndShowNotification = async (sw, payload, currentNotification) => {
const { data: incomingData } = payload.options
const { tag, data: currentData } = currentNotification
// how many notification with this tag are there already?
// (start from 2 and +1 to include incoming notification)
const amount = currentNotification.data?.amount ? currentNotification.data.amount + 1 : 2
let title = ''
let newData = {}
if (tag === 'REPLY') {
title = `you have ${amount} new replies`
} else if (tag === 'MENTION') {
title = `you were mentioned ${amount} times`
} else if (tag === 'REFERRAL') {
title = `${amount} stackers joined via your referral links`
} else if (tag === 'INVITE') {
title = `your invite has been redeemed by ${amount} stackers`
} else if (tag === 'DEPOSIT') {
const currentSats = currentData.sats
const incomingSats = incomingData.sats
const newSats = currentSats + incomingSats
title = `${numWithUnits(newSats, { abbreviate: false })} were deposited in your account`
newData.sats = newSats
} else if (tag.split('-')[0] === 'FOLLOW') {
const { followeeName, subType } = incomingData
title = `@${followeeName} ${subType === 'POST' ? `created ${amount} posts` : `replied ${amount} times`}`
newData = incomingData
const mergeAndShowNotification = async (sw, payload, currentNotifications, tag) => {
// sanity check
const otherTagNotifications = currentNotifications.filter(({ tag: nTag }) => nTag !== tag)
if (otherTagNotifications.length > 0) {
// we can't recover from this here. bail.
const message = `[sw:push] more than one notification with tag ${tag} after filter`
messageChannelPort?.postMessage({ level: 'error', message })
console.error(message)
return
}
// close current notification before showing new one to "merge" notifications
currentNotification.close()
const newNotificationOptions = { icon: currentNotification.icon, tag, data: { url: '/notifications', amount, ...newData } }
return await sw.registration.showNotification(title, newNotificationOptions)
const { data: incomingData } = payload.options
// we can ignore everything after the first dash in the tag for our control flow
const compareTag = tag.split('-')[0]
// merge notifications into single notification payload
// ---
// tags that need to know the amount of notifications with same tag for merging
const AMOUNT_TAGS = ['REPLY', 'MENTION', 'REFERRAL', 'INVITE', 'FOLLOW']
// tags that need to know the sum of sats of notifications with same tag for merging
const SUM_SATS_TAGS = ['DEPOSIT']
// this should reflect the amount of notifications that were already merged before
const initialAmount = currentNotifications[0].data?.amount || 1
const mergedPayload = currentNotifications.reduce((acc, { data }) => {
let newAmount, newSats
if (AMOUNT_TAGS.includes(compareTag)) {
newAmount = acc.amount + 1
}
if (SUM_SATS_TAGS.includes(compareTag)) {
newSats = acc.sats + data.sats
}
const newPayload = { ...data, amount: newAmount, sats: newSats }
return newPayload
}, { ...incomingData, amount: initialAmount })
// calculate title from merged payload
const { amount, followeeName, subType, sats } = mergedPayload
let title = ''
if (AMOUNT_TAGS.includes(compareTag)) {
if (compareTag === 'REPLY') {
title = `you have ${amount} new replies`
} else if (compareTag === 'MENTION') {
title = `you were mentioned ${amount} times`
} else if (compareTag === 'REFERRAL') {
title = `${amount} stackers joined via your referral links`
} else if (compareTag === 'INVITE') {
title = `your invite has been redeemed by ${amount} stackers`
} else if (compareTag === 'FOLLOW') {
title = `@${followeeName} ${subType === 'POST' ? `created ${amount} posts` : `replied ${amount} times`}`
}
} else if (SUM_SATS_TAGS.includes(compareTag)) {
// there is only DEPOSIT in this array
title = `${numWithUnits(sats, { abbreviate: false })} were deposited in your account`
}
// close all current notifications before showing new one to "merge" notifications
currentNotifications.forEach(n => n.close())
const options = { icon: payload.options?.icon, tag, data: { url: '/notifications', ...mergedPayload } }
return await sw.registration.showNotification(title, options)
}
export function onNotificationClick (sw) {