stacker.news/components/serviceworker.js
ekzyis 15fb7f446b
Wallet Logs (#994)
* nwc wallet logs

* persist logs in IndexedDB

* Potential fix for empty error message

* load logs limited to 5m ago from IDB

* load logs from past via query param

* Add 5m, 1h, 6h links for earlier logs

* Show end of log

* Clamp to logStart

* Add log.module.css

* Remove TODO about persistence

* Use table for logs

* <table> fixes bad format with fixed width and message overflow into start of next row
* also using ---start of log--- instead of ---end of log--- now
* removed time string in header nav

* Rename .header to .logNav

* Simply load all logs and remove navigation

I realized the code for navigation was most likely premature optimization which even resulted in worse UX:
Using the buttons to load logs from 5m, 1h, 6h ago sometimes meant that nothing happened at all since there were no logs from 5m, 1h, 6h ago.
That's why I added a time string as "start of logs" so it's at least visible that it changed but that looked bad so I removed it.

But all of this was not necessary: I can simply load all logs at once and then the user can scroll around however they like.

I was worried that it would be bad for performance to load all logs at once since we might store a lot of logs but as mentioned, that's probably premature optimization.

WHEN a lot of logs are stored AND this becomes a problem (What problem even? Slow page load?), THEN we can think about this.

If page load ever becomes slow because of loading logs, we could probably simply not load the logs at page load but only when /wallet/logs is visited.

But for now, this works fine.

* Add follow checkbox

* Create WalletLogs component

* Embed wallet logs

* Remove test error

* Fix level padding

* Add LNbits logs

* Add logs for attaching LND and lnAddr

* Use err.message || err.toString?.() consistently

* Autowithdrawal logs

* Use details from LND error

* Don't log test invoice individually

* Also refetch logs on error

* Remove obsolete and annoying toasts

* Replace scrollIntoView with scroll

* Use constant embedded max-height

* Fix missing width: 100% for embedded logs

* Show full payment hash and preimage in logs

* Also parse details from LND errors on autowithdrawal failures

* Remove TODO

* Fix accidental removal of wss:// check

* Fix alignment of start marker and show empty if empty

* Fix sendPayment loop

* Split context in two
2024-04-03 17:27:21 -05:00

184 lines
7.7 KiB
JavaScript

import { createContext, useContext, useEffect, useState, useCallback, useMemo } from 'react'
import { Workbox } from 'workbox-window'
import { gql, useMutation } from '@apollo/client'
import { detectOS, useServiceWorkerLogger } from './logger'
const applicationServerKey = process.env.NEXT_PUBLIC_VAPID_PUBKEY
const ServiceWorkerContext = createContext()
// message types for communication between app and service worker
export const MESSAGE_PORT = 'MESSAGE_PORT' // message to exchange message channel on which service worker will send messages back to app
export const ACTION_PORT = 'ACTION_PORT' // message to exchange action channel on which service worker will send actions back to app
export const SYNC_SUBSCRIPTION = 'SYNC_SUBSCRIPTION' // trigger onPushSubscriptionChange event in service worker manually
export const RESUBSCRIBE = 'RESUBSCRIBE' // trigger resubscribing to push notifications (sw -> app)
export const DELETE_SUBSCRIPTION = 'DELETE_SUBSCRIPTION' // delete subscription in IndexedDB (app -> sw)
export const STORE_SUBSCRIPTION = 'STORE_SUBSCRIPTION' // store subscription in IndexedDB (app -> sw)
export const STORE_OS = 'STORE_OS' // store OS in service worker
export const ServiceWorkerProvider = ({ children }) => {
const [registration, setRegistration] = useState(null)
const [support, setSupport] = useState({ serviceWorker: undefined, pushManager: undefined })
const [permission, setPermission] = useState({ notification: undefined })
const [savePushSubscription] = useMutation(
gql`
mutation savePushSubscription(
$endpoint: String!
$p256dh: String!
$auth: String!
) {
savePushSubscription(
endpoint: $endpoint
p256dh: $p256dh
auth: $auth
) {
id
}
}
`)
const [deletePushSubscription] = useMutation(
gql`
mutation deletePushSubscription($endpoint: String!) {
deletePushSubscription(endpoint: $endpoint) {
id
}
}
`)
const logger = useServiceWorkerLogger()
// I am not entirely sure if this is needed since at least in Brave,
// using `registration.pushManager.subscribe` also prompts the user.
// However, I am keeping this here since that's how it's done in most guides.
// Could be that this is required for the `registration.showNotification` call
// to work or that some browsers will break without this.
const requestNotificationPermission = useCallback(() => {
// https://web.dev/push-notifications-subscribing-a-user/#requesting-permission
return new Promise(function (resolve, reject) {
const permission = window.Notification.requestPermission(function (result) {
resolve(result)
})
if (permission) {
permission.then(resolve, reject)
}
}).then(function (permission) {
setPermission({ notification: permission })
if (permission === 'granted') return subscribeToPushNotifications()
})
})
const subscribeToPushNotifications = async () => {
// serviceWorker.controller is null on forced refreshes
// see https://w3c.github.io/ServiceWorker/#navigator-service-worker-controller
if (!navigator.serviceWorker.controller) {
throw new Error('no active service worker found. try refreshing page.')
}
const subscribeOptions = { userVisibleOnly: true, applicationServerKey }
// Brave users must enable a flag in brave://settings/privacy first
// see https://stackoverflow.com/a/69624651
let pushSubscription = await registration.pushManager.subscribe(subscribeOptions)
const { endpoint } = pushSubscription
logger.info('subscribed to push notifications', { endpoint })
// convert keys from ArrayBuffer to string
pushSubscription = JSON.parse(JSON.stringify(pushSubscription))
// Send subscription to service worker to save it so we can use it later during `pushsubscriptionchange`
// see https://medium.com/@madridserginho/how-to-handle-webpush-api-pushsubscriptionchange-event-in-modern-browsers-6e47840d756f
navigator.serviceWorker.controller.postMessage({
action: STORE_SUBSCRIPTION,
subscription: pushSubscription
})
logger.info('sent STORE_SUBSCRIPTION to service worker', { endpoint })
// send subscription to server
const variables = {
endpoint,
p256dh: pushSubscription.keys.p256dh,
auth: pushSubscription.keys.auth
}
await savePushSubscription({ variables })
logger.info('sent push subscription to server', { endpoint })
}
const unsubscribeFromPushNotifications = async (subscription) => {
await subscription.unsubscribe()
const { endpoint } = subscription
logger.info('unsubscribed from push notifications', { endpoint })
await deletePushSubscription({ variables: { endpoint } })
// also delete push subscription in IndexedDB so we can tell if the user disabled push subscriptions
// or we lost the push subscription due to a bug
navigator.serviceWorker.controller.postMessage({ action: DELETE_SUBSCRIPTION })
logger.info('deleted push subscription from server', { endpoint })
}
const togglePushSubscription = useCallback(async () => {
const pushSubscription = await registration.pushManager.getSubscription()
if (pushSubscription) {
return unsubscribeFromPushNotifications(pushSubscription)
}
return subscribeToPushNotifications().then(async () => {
// request persistent storage: https://web.dev/learn/pwa/offline-data#data_persistence
const persisted = await navigator?.storage?.persisted?.()
if (!persisted && navigator?.storage?.persist) {
return navigator.storage.persist().then(persistent => {
logger.info('persistent storage:', persistent)
}).catch(logger.error)
}
})
})
useEffect(() => {
setSupport({
serviceWorker: 'serviceWorker' in navigator,
notification: 'Notification' in window,
pushManager: 'PushManager' in window
})
setPermission({ notification: 'Notification' in window ? window.Notification.permission : 'denied' })
if (!('serviceWorker' in navigator)) {
logger.info('device does not support service worker')
return
}
const wb = new Workbox('/sw.js', { scope: '/' })
wb.register().then(registration => {
logger.info('service worker registration successful')
setRegistration(registration)
})
}, [])
useEffect(() => {
// wait until successful registration
if (!registration) return
// setup channel between app and service worker
const channel = new MessageChannel()
navigator?.serviceWorker?.controller?.postMessage({ action: ACTION_PORT }, [channel.port2])
channel.port1.onmessage = (event) => {
if (event.data.action === RESUBSCRIBE) {
return subscribeToPushNotifications()
}
}
// since (a lot of) browsers don't support the pushsubscriptionchange event,
// we sync with server manually by checking on every page reload if the push subscription changed.
// see https://medium.com/@madridserginho/how-to-handle-webpush-api-pushsubscriptionchange-event-in-modern-browsers-6e47840d756f
navigator?.serviceWorker?.controller?.postMessage?.({ action: SYNC_SUBSCRIPTION })
logger.info('sent SYNC_SUBSCRIPTION to service worker')
navigator?.serviceWorker?.controller?.postMessage?.({ action: STORE_OS, os: detectOS() })
}, [registration])
const contextValue = useMemo(() => ({
registration,
support,
permission,
requestNotificationPermission,
togglePushSubscription
}), [registration, support, permission, requestNotificationPermission, togglePushSubscription])
return (
<ServiceWorkerContext.Provider value={contextValue}>
{children}
</ServiceWorkerContext.Provider>
)
}
export function useServiceWorker () {
return useContext(ServiceWorkerContext)
}