268 lines
9.2 KiB
Plaintext
268 lines
9.2 KiB
Plaintext
---
|
|
import { Icon } from 'astro-icon/components'
|
|
import { VAPID_PUBLIC_KEY } from 'astro:env/server'
|
|
|
|
import { SUPPORT_EMAIL } from '../constants/project'
|
|
import { cn } from '../lib/cn'
|
|
|
|
import Button from './Button.astro'
|
|
|
|
import type { Prisma } from '@prisma/client'
|
|
import type { HTMLAttributes } from 'astro/types'
|
|
|
|
type Props = HTMLAttributes<'div'> & {
|
|
dismissable?: boolean
|
|
hideIfEnabled?: boolean
|
|
pushSubscriptions: Prisma.PushSubscriptionGetPayload<{
|
|
select: {
|
|
endpoint: true
|
|
userAgent: true
|
|
}
|
|
}>[]
|
|
}
|
|
|
|
const { class: className, dismissable = false, pushSubscriptions, hideIfEnabled, ...props } = Astro.props
|
|
---
|
|
|
|
<div
|
|
data-push-notification-banner
|
|
data-dismissed={undefined as true | undefined /* Updated by client script */}
|
|
data-notification-supported={undefined as true | undefined /* Updated by client script */}
|
|
data-push-subscriptions={JSON.stringify(pushSubscriptions)}
|
|
data-is-enabled={undefined as true | undefined /* Updated by client script */}
|
|
data-loaded={undefined as true | undefined /* Updated by client script */}
|
|
data-notification-permissions={undefined as
|
|
| NotificationPermission
|
|
| undefined /* Updated by client script */}
|
|
data-vapid-public-key={VAPID_PUBLIC_KEY}
|
|
class={cn(
|
|
'no-js:hidden xs:grid-cols-[auto_auto] xs:justify-between relative isolate grid items-center justify-stretch gap-4 overflow-hidden rounded-xl bg-gradient-to-r from-blue-950/80 to-blue-900/60 p-4 not-data-loaded:hidden',
|
|
'data-dismissed:hidden',
|
|
hideIfEnabled && 'data-is-enabled:hidden',
|
|
'not-data-notification-supported:hidden',
|
|
'data-is-enabled:**:data-show-if-disabled:hidden not-data-is-enabled:**:data-show-if-enabled:hidden',
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<div aria-hidden="true" class="pointer-events-none absolute inset-0 -z-10 overflow-hidden">
|
|
<div
|
|
class="absolute top-0 -left-16 h-full w-1/3 bg-gradient-to-r from-blue-500/20 to-transparent opacity-50 blur-xl"
|
|
>
|
|
</div>
|
|
<div
|
|
class="absolute top-0 -right-16 h-full w-1/3 bg-gradient-to-l from-blue-500/20 to-transparent opacity-50 blur-xl"
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-x-3">
|
|
<div class="rounded-md bg-blue-800/30 p-2">
|
|
<Icon name="ri:notification-4-line" class="size-5 text-blue-300" />
|
|
</div>
|
|
<div>
|
|
<h3 class="font-medium text-blue-100" data-banner-title>Turn on push notifications?</h3>
|
|
<p class="text-sm text-blue-200/80" data-banner-description>Get notifications on this device.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="xs:justify-end flex shrink flex-wrap items-center justify-center gap-2">
|
|
{dismissable && <Button as="span" label="Skip" variant="faded" data-dismiss-button />}
|
|
<Button
|
|
as="span"
|
|
label="Yes, notify me"
|
|
color="white"
|
|
data-push-action="subscribe"
|
|
data-show-if-disabled
|
|
/>
|
|
<Button
|
|
as="span"
|
|
label="Stop notifications"
|
|
color="white"
|
|
variant="faded"
|
|
data-push-action="unsubscribe"
|
|
data-show-if-enabled
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
class="col-span-full flex items-center justify-center gap-2 leading-tight text-red-500 has-[[data-error-message]:empty]:hidden"
|
|
>
|
|
<Icon name="ri:error-warning-line" class="size-5 shrink-0" />
|
|
<span>
|
|
<span data-error-message></span>
|
|
<a href={`mailto:${SUPPORT_EMAIL}`} class="text-red-300 underline hover:text-red-200">
|
|
Contact support
|
|
</a>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
/////////////////////////////////////////////////////////////
|
|
// Script to handle push notification banner dismissal. //
|
|
/////////////////////////////////////////////////////////////
|
|
|
|
import { typedLocalStorage } from '../lib/localstorage'
|
|
|
|
document.addEventListener('astro:page-load', () => {
|
|
let pushNotificationsBannerDismissedAt = typedLocalStorage.pushNotificationsBannerDismissedAt.get()
|
|
|
|
if (
|
|
pushNotificationsBannerDismissedAt &&
|
|
pushNotificationsBannerDismissedAt < new Date(Date.now() - 1000 * 60 * 60 * 24 * 365) // 1 year
|
|
) {
|
|
typedLocalStorage.pushNotificationsBannerDismissedAt.remove()
|
|
pushNotificationsBannerDismissedAt = undefined
|
|
}
|
|
|
|
document.querySelectorAll<HTMLElement>('[data-push-notification-banner]').forEach((banner) => {
|
|
const skipButton = banner.querySelector<HTMLElement>('[data-dismiss-button]')
|
|
if (!skipButton) return
|
|
|
|
if (pushNotificationsBannerDismissedAt) {
|
|
banner.dataset.dismissed = ''
|
|
}
|
|
|
|
skipButton.addEventListener('click', (event) => {
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
|
|
banner.dataset.dismissed = ''
|
|
|
|
const now = new Date()
|
|
typedLocalStorage.pushNotificationsBannerDismissedAt.set(now)
|
|
pushNotificationsBannerDismissedAt = now
|
|
})
|
|
})
|
|
})
|
|
</script>
|
|
|
|
<script>
|
|
///////////////////////////////////////////
|
|
// Script to handle push notifications. //
|
|
///////////////////////////////////////////
|
|
|
|
import {
|
|
subscribeToPushNotifications,
|
|
unsubscribeFromPushNotifications,
|
|
parsePushSubscriptions,
|
|
isCurrentDeviceSubscribed,
|
|
supportsPushNotifications,
|
|
type SafeResult,
|
|
} from '../lib/client/clientPushNotifications'
|
|
|
|
import {
|
|
enableBrowserNotifications,
|
|
disableBrowserNotifications,
|
|
isBrowserNotificationsEnabled,
|
|
supportsBrowserNotifications,
|
|
} from '../lib/client/browserNotifications'
|
|
|
|
async function setDataAttributes(banner: HTMLElement) {
|
|
if (!supportsPushNotifications() && !supportsBrowserNotifications()) {
|
|
return
|
|
}
|
|
|
|
banner.dataset.notificationSupported = ''
|
|
banner.dataset.notificationPermissions = Notification.permission
|
|
|
|
const serverSubscriptions = parsePushSubscriptions(banner.dataset.pushSubscriptions)
|
|
const titleElement = banner.querySelector<HTMLElement>('[data-banner-title]')
|
|
const descriptionElement = banner.querySelector<HTMLElement>('[data-banner-description]')
|
|
|
|
if (await isCurrentDeviceSubscribed(serverSubscriptions)) {
|
|
if (titleElement) titleElement.textContent = 'Push notifications enabled'
|
|
if (descriptionElement) descriptionElement.textContent = 'Turn push notifications off for this device?'
|
|
banner.dataset.isEnabled = ''
|
|
banner.dataset.loaded = ''
|
|
return
|
|
}
|
|
|
|
if (isBrowserNotificationsEnabled()) {
|
|
if (titleElement) titleElement.textContent = 'Browser notifications enabled'
|
|
if (descriptionElement)
|
|
descriptionElement.textContent = 'Turn off notifications? (They only work while the tab is open.)'
|
|
banner.dataset.isEnabled = ''
|
|
banner.dataset.loaded = ''
|
|
return
|
|
}
|
|
|
|
// Default state (disabled)
|
|
if (titleElement) titleElement.textContent = 'Turn on push notifications?'
|
|
if (descriptionElement) descriptionElement.textContent = 'Get notifications on this device.'
|
|
banner.dataset.loaded = ''
|
|
}
|
|
|
|
document.addEventListener('astro:page-load', async () => {
|
|
document.querySelectorAll<HTMLElement>('[data-push-notification-banner]').forEach(async (banner) => {
|
|
await setDataAttributes(banner)
|
|
|
|
const vapidPublicKey = banner.dataset.vapidPublicKey
|
|
|
|
banner.querySelectorAll<HTMLElement>('[data-push-action]').forEach((button) => {
|
|
button.addEventListener('click', async (event) => {
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
|
|
const action = button.dataset.pushAction
|
|
if (action !== 'subscribe' && action !== 'unsubscribe') {
|
|
console.error('Invalid push action:', action)
|
|
return
|
|
}
|
|
|
|
let result: SafeResult
|
|
|
|
if (action === 'subscribe') {
|
|
const pushResult = vapidPublicKey
|
|
? await subscribeToPushNotifications(vapidPublicKey)
|
|
: { success: false as const, error: 'VAPID public key not found' }
|
|
|
|
if (pushResult.success) {
|
|
result = pushResult
|
|
} else {
|
|
console.error(
|
|
"Can't enable push notifications, trying browser notifications.",
|
|
pushResult.error
|
|
)
|
|
const browserResult = await enableBrowserNotifications()
|
|
|
|
if (browserResult.success) {
|
|
result = browserResult
|
|
} else {
|
|
console.error("Can't enable browser notifications:", browserResult.error)
|
|
result = {
|
|
success: false,
|
|
error: `Can't enable either push or browser notifications. Push: ${pushResult.error}. Browser: ${browserResult.error}`,
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
const pushResult = await unsubscribeFromPushNotifications()
|
|
const browserResult = disableBrowserNotifications()
|
|
|
|
const success = pushResult.success || browserResult.success
|
|
|
|
result = success
|
|
? { success: true }
|
|
: { success: false, error: `${pushResult.error} | ${browserResult.error}` }
|
|
}
|
|
|
|
if (!result.success) {
|
|
console.error(result.error)
|
|
|
|
const errorMessageElement = banner.querySelector<HTMLElement>('[data-error-message]')
|
|
if (errorMessageElement) {
|
|
errorMessageElement.textContent = `Failed to ${action === 'subscribe' ? 'enable' : 'disable'} notifications.`
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
window.location.reload()
|
|
})
|
|
})
|
|
})
|
|
})
|
|
</script>
|