Files
session-notify/src/client/platform.js
T

106 lines
4.0 KiB
JavaScript

/**
* Every browser and platform fact this plugin reads, in one seam.
*
* Nothing here owns state or decides anything: the window's visibility, the
* notification permission, the notification icon, and the one platform hint the
* settings copy uses. Each read is contained, because a page a plugin does not
* control may hide any of them — and because a contained read is what lets the
* delivery path keep going when the answer is unreadable.
*
* @module @dsh-plugin/session-notify/client/platform
*/
/**
* The window's Notification constructor, or undefined when this environment has
* none (a plain Node test, or a browser without the API).
* @returns the constructor, or undefined.
*/
export function notificationApi() {
const Ctor = globalThis.Notification
return typeof Ctor === 'function' ? Ctor : undefined
}
/** Permission lookup that never throws in a browser without the API. */
export function notificationPermission() {
const Ctor = notificationApi()
if (Ctor === undefined) return 'unsupported'
const permission = Ctor.permission
if (permission === 'granted' || permission === 'denied' || permission === 'default') return permission
return 'default'
}
/**
* Whether the Harness window is somewhere the user cannot see the app.
*
* Two independent browser facts are consulted, and ANY of them counts as "not
* in the foreground": the page's visibility and the document's focus. They are
* read at delivery time rather than remembered from an event, because a missed
* blur in the desktop shell made this plugin believe the window was in front
* and swallow the alert.
* @returns whether the alert must go to the system notification channel.
*/
export function windowIsAway() {
try {
if (document.visibilityState === 'hidden') return true
} catch {
/* an unreadable visibility state leaves the focus fact */
}
try {
if (typeof document.hasFocus === 'function') return !document.hasFocus()
} catch {
/* an unreadable focus state leaves the default below */
}
return false
}
/** Read the window's live focus state rather than a cached value. */
export function currentFocus() {
return !windowIsAway()
}
/** Focus the window without letting a refusal stop the navigation. */
export function focusWindow() {
try {
globalThis.focus?.()
} catch {
/* focus is best effort on every platform */
}
}
/**
* The system-notification icon, built once when this bundle materializes: this
* deployment's own static URL when the page serves this package's assets, else
* the SVG inline. Icons are cosmetic on every platform, so each step is
* contained — and materialization happens on first import, not when the bundle
* script runs, so a page that never activates the plugin never gets here.
* @type {string | undefined} an icon URL, or undefined when neither form works.
*/
export const systemIcon = (() => {
try {
const url = new URL('./icon.svg', document.baseURI).href
if (url !== '') return url
} catch {
/* fall through to the inline form */
}
try {
const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M8 2.35C5.7 2.35 3.83 4.22 3.83 6.52V9.02L3.06 11.1C2.99 11.29 3.13 11.48 3.33 11.48H12.67C12.87 11.48 13.01 11.29 12.94 11.1L12.17 9.02V6.52C12.17 4.22 10.3 2.35 8 2.35Z" fill="none" stroke="%23247bbf" stroke-width="1.2"/><path d="M6.3 12.75C6.52 13.66 7.2 14.28 8 14.28C8.8 14.28 9.48 13.66 9.7 12.75" fill="none" stroke="%23247bbf" stroke-width="1.2" stroke-linecap="round"/></svg>'
return `data:image/svg+xml,${svg}`
} catch {
return undefined
}
})()
/** One best-effort platform hint, used for settings help text only. */
export function platformKey() {
try {
const agent = navigator.userAgentData
const platform = String((agent?.platform ?? navigator.platform) || navigator.userAgent || '')
if (/win/i.test(platform)) return 'win'
if (/mac|iphone|ipad/i.test(platform)) return 'mac'
if (/linux|x11/i.test(platform)) return 'linux'
} catch {
/* an unreadable navigator keeps the neutral hint */
}
return 'plain'
}