♻️ refactor(client): 浏览器半拆成 src 模块,client.js 改由构建产出
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* The system-notification channel: raising one, and reporting what the platform
|
||||
* actually did with it.
|
||||
*
|
||||
* @module @dsh-plugin/session-notify/client/core/system-channel
|
||||
*/
|
||||
|
||||
import { RAISED_LIMIT } from '../constants.js'
|
||||
import { focusWindow, notificationPermission, systemIcon } from '../platform.js'
|
||||
import { text } from './log.js'
|
||||
|
||||
/**
|
||||
* Build the system channel over one store.
|
||||
* @param deps - the plugin store; the copy table; `notify` for contained
|
||||
* diagnostics; `openSession` for a click that must land on the conversation;
|
||||
* and `dropReplay`, so clicking the banner cancels its in-app replay.
|
||||
* @returns the channel's operations.
|
||||
*/
|
||||
export function createSystemChannel({ store, copy, notify, openSession, dropReplay }) {
|
||||
/** System notifications this page still holds open, so nothing collects them mid-display. */
|
||||
const raised = new Set()
|
||||
|
||||
/**
|
||||
* The system-notification channel's own answer about one notification.
|
||||
*
|
||||
* A browser reports the platform's verdict asynchronously: `show` means the
|
||||
* notification reached the desktop, `error` means the platform refused it, and
|
||||
* silence means neither answer arrived. That third case is the one that used to
|
||||
* lose alerts silently, so it is recorded rather than assumed to be a success.
|
||||
* @param monitor - the record returned by {@link notifySystem} for this notification.
|
||||
* @param shown - `true` when the platform displayed it, `false` when it refused.
|
||||
*/
|
||||
const confirmDelivery = (monitor, shown) => {
|
||||
if (monitor === null || monitor === undefined || typeof monitor !== 'object') return
|
||||
monitor.shown = shown
|
||||
const snapshot = store.getSnapshot()
|
||||
const last = snapshot.lastDelivery
|
||||
if (last === null || last === undefined || last.monitor !== monitor || last.shown === shown) return
|
||||
store.set({ ...snapshot, lastDelivery: { ...last, shown } })
|
||||
}
|
||||
|
||||
/** Hold one raised notification open, bounded, so it cannot be collected while pending. */
|
||||
const keepRaised = (notification) => {
|
||||
raised.add(notification)
|
||||
while (raised.size > RAISED_LIMIT) {
|
||||
const oldest = raised.values().next().value
|
||||
if (oldest === undefined) break
|
||||
raised.delete(oldest)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Raise one system notification, tolerating every refusal a browser may give,
|
||||
* and report what the platform said about it.
|
||||
* @param candidate - the alert to raise.
|
||||
* @param force - raise it even when the permission is not granted (the settings
|
||||
* page's own test button, which exists to prove the channel on its own).
|
||||
* @returns a record whose `outcome` is the channel's verdict (`raised`,
|
||||
* `permission`, `unsupported`, or `threw`) and whose `shown` is filled in
|
||||
* later by {@link confirmDelivery} — `true` displayed, `false` refused,
|
||||
* `undefined` while the platform has said nothing.
|
||||
*/
|
||||
const notifySystem = (candidate, force) => {
|
||||
const Ctor = globalThis.Notification
|
||||
const permission = notificationPermission()
|
||||
if (permission === 'unsupported') return { outcome: 'unsupported' }
|
||||
if (!force && permission !== 'granted') return { outcome: 'permission' }
|
||||
try {
|
||||
const notice = copy.copyFor(candidate)
|
||||
const options = {
|
||||
body: notice.body,
|
||||
tag: `dsh-session-${candidate.sessionId}`,
|
||||
requireInteraction: false,
|
||||
}
|
||||
if (systemIcon !== undefined) options.icon = systemIcon
|
||||
const notification = new Ctor(notice.title, options)
|
||||
const record = { outcome: 'raised', notice, shown: undefined }
|
||||
keepRaised(notification)
|
||||
notification.onshow = () => {
|
||||
confirmDelivery(record, true)
|
||||
}
|
||||
notification.onerror = (event) => {
|
||||
confirmDelivery(record, false)
|
||||
notify(`system notification was refused: ${String(event?.message ?? 'unknown')}`)
|
||||
}
|
||||
notification.onclose = () => {
|
||||
raised.delete(notification)
|
||||
}
|
||||
notification.onclick = () => {
|
||||
dropReplay(candidate)
|
||||
focusWindow()
|
||||
openSession(candidate.sessionId)
|
||||
try {
|
||||
notification.close()
|
||||
} catch {
|
||||
/* close is best effort on every platform */
|
||||
}
|
||||
}
|
||||
return record
|
||||
} catch (error) {
|
||||
notify(`system notification failed: ${text(error)}`)
|
||||
return { outcome: 'threw' }
|
||||
}
|
||||
}
|
||||
|
||||
/** Drop the held references: the plugin is being disposed. */
|
||||
const dispose = () => {
|
||||
raised.clear()
|
||||
}
|
||||
|
||||
return { notifySystem, confirmDelivery, dispose }
|
||||
}
|
||||
Reference in New Issue
Block a user