mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-30 05:25:00 +02:00
Handle blocked audio and invalid VAPID keys (#11046)
* Handle blocked audio and invalid VAPID keys Signed-off-by: Artyom Savchenko <armisav@gmail.com> * Fix build issues Signed-off-by: Artyom Savchenko <armisav@gmail.com> --------- Signed-off-by: Artyom Savchenko <armisav@gmail.com>
This commit is contained in:
Generated
+3
@@ -14824,6 +14824,9 @@ importers:
|
||||
'@types/jest':
|
||||
specifier: ^29.5.5
|
||||
version: 29.5.14
|
||||
'@types/node':
|
||||
specifier: ^22.18.1
|
||||
version: 22.19.0
|
||||
'@types/png-chunks-extract':
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/node": "^22.18.1",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"jest-environment-jsdom": "^30.2.0",
|
||||
"svelte-eslint-parser": "^0.33.1",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getClient } from '.'
|
||||
import notification from '@hcengineering/notification'
|
||||
|
||||
const sounds = new Map<Asset, AudioBuffer>()
|
||||
const resumeTimeoutMs = 1000
|
||||
let context: AudioContext | undefined
|
||||
|
||||
function getAudioContext (): AudioContext {
|
||||
@@ -11,6 +12,36 @@ function getAudioContext (): AudioContext {
|
||||
return context
|
||||
}
|
||||
|
||||
function isAudioContextRunning (context: AudioContext): boolean {
|
||||
return context.state === 'running'
|
||||
}
|
||||
|
||||
async function resumeAudioContext (context: AudioContext): Promise<boolean> {
|
||||
if (context.state === 'running') return true
|
||||
if (context.state === 'closed') return false
|
||||
|
||||
// Calling resume before the document has received user activation can leave
|
||||
// its promise pending indefinitely because of the browser autoplay policy.
|
||||
if (navigator.userActivation?.hasBeenActive === false) return false
|
||||
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined
|
||||
try {
|
||||
const resumed = await Promise.race([
|
||||
context.resume().then(() => true),
|
||||
new Promise<boolean>((resolve) => {
|
||||
timeout = setTimeout(() => {
|
||||
resolve(false)
|
||||
}, resumeTimeoutMs)
|
||||
})
|
||||
])
|
||||
return resumed && isAudioContextRunning(context)
|
||||
} catch {
|
||||
return false
|
||||
} finally {
|
||||
if (timeout !== undefined) clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
|
||||
export async function isNotificationAllowed (_class?: Ref<Class<Doc>>): Promise<boolean> {
|
||||
if (_class === undefined) return false
|
||||
const client = getClient()
|
||||
@@ -52,9 +83,8 @@ export async function playSound (soundKey: string, loop = false): Promise<(() =>
|
||||
|
||||
try {
|
||||
const context = getAudioContext()
|
||||
if (context.state === 'suspended') {
|
||||
await context.resume()
|
||||
}
|
||||
if (!(await resumeAudioContext(context))) return null
|
||||
|
||||
const audio = context.createBufferSource()
|
||||
audio.buffer = sound
|
||||
audio.loop = loop
|
||||
|
||||
@@ -201,7 +201,9 @@ export function createWorkspaceFavicon (ownerDocument: Document = document): {
|
||||
let disposed = false
|
||||
let request: AbortController | undefined
|
||||
function removeDefaultLinks (): void {
|
||||
for (const icon of ownerDocument.head.querySelectorAll('link[rel~="icon"]:not(#workspace-favicon)')) icon.remove()
|
||||
for (const icon of Array.from(ownerDocument.head.querySelectorAll('link[rel~="icon"]:not(#workspace-favicon)'))) {
|
||||
icon.remove()
|
||||
}
|
||||
}
|
||||
function restore (): void {
|
||||
link.remove()
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./types"
|
||||
"declarationDir": "./types",
|
||||
"types": ["node", "jest"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "lib", "dist", "types", "bundle"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -735,7 +735,7 @@ export function pushAvailable (): boolean {
|
||||
return (
|
||||
'serviceWorker' in navigator &&
|
||||
'PushManager' in window &&
|
||||
isValidPushPublicKey(publicKey) &&
|
||||
hasValidPushPublicKeyFormat(publicKey) &&
|
||||
'Notification' in window &&
|
||||
Notification.permission !== 'denied'
|
||||
)
|
||||
@@ -748,7 +748,12 @@ export async function subscribePush (): Promise<boolean> {
|
||||
}
|
||||
const client = getClient()
|
||||
const publicKey = getPushPublicKey()
|
||||
if ('serviceWorker' in navigator && 'PushManager' in window && isValidPushPublicKey(publicKey)) {
|
||||
if ('serviceWorker' in navigator && 'PushManager' in window && hasValidPushPublicKeyFormat(publicKey)) {
|
||||
if (!(await isValidPushPublicKey(publicKey))) {
|
||||
pushAllowed.set(false)
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const loc = getCurrentLocation()
|
||||
let registration = await navigator.serviceWorker.getRegistration(`/${loc.path[0]}/${loc.path[1]}`)
|
||||
@@ -808,7 +813,7 @@ function getPushPublicKey (): string | undefined {
|
||||
return publicKey.trim() !== '' ? publicKey : undefined
|
||||
}
|
||||
|
||||
function isValidPushPublicKey (publicKey: string | undefined): publicKey is string {
|
||||
function hasValidPushPublicKeyFormat (publicKey: string | undefined): publicKey is string {
|
||||
if (publicKey === undefined || publicKey.trim() === '') return false
|
||||
|
||||
try {
|
||||
@@ -819,6 +824,23 @@ function isValidPushPublicKey (publicKey: string | undefined): publicKey is stri
|
||||
}
|
||||
}
|
||||
|
||||
async function isValidPushPublicKey (publicKey: string): Promise<boolean> {
|
||||
if (globalThis.crypto?.subtle === undefined) return false
|
||||
|
||||
try {
|
||||
await globalThis.crypto.subtle.importKey(
|
||||
'raw',
|
||||
urlBase64ToUint8Array(publicKey),
|
||||
{ name: 'ECDSA', namedCurve: 'P-256' },
|
||||
false,
|
||||
['verify']
|
||||
)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function urlBase64ToUint8Array (value: string): Uint8Array<ArrayBuffer> {
|
||||
const padding = '='.repeat((4 - (value.length % 4)) % 4)
|
||||
const base64 = `${value}${padding}`.replace(/-/g, '+').replace(/_/g, '/')
|
||||
|
||||
Reference in New Issue
Block a user