mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-22 08:20:39 +00:00
Improve push notifications (#5397)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
parent
a948b20155
commit
83725fc541
@ -20,6 +20,7 @@ import {
|
|||||||
DOMAIN_MODEL,
|
DOMAIN_MODEL,
|
||||||
Hierarchy,
|
Hierarchy,
|
||||||
IndexKind,
|
IndexKind,
|
||||||
|
type Space,
|
||||||
type Account,
|
type Account,
|
||||||
type AttachedDoc,
|
type AttachedDoc,
|
||||||
type Class,
|
type Class,
|
||||||
@ -60,8 +61,8 @@ import {
|
|||||||
type CommonInboxNotification,
|
type CommonInboxNotification,
|
||||||
type CommonNotificationType,
|
type CommonNotificationType,
|
||||||
type DocNotifyContext,
|
type DocNotifyContext,
|
||||||
type DocUpdates,
|
|
||||||
type DocUpdateTx,
|
type DocUpdateTx,
|
||||||
|
type DocUpdates,
|
||||||
type InboxNotification,
|
type InboxNotification,
|
||||||
type MentionInboxNotification,
|
type MentionInboxNotification,
|
||||||
type NotificationContextPresenter,
|
type NotificationContextPresenter,
|
||||||
@ -73,8 +74,8 @@ import {
|
|||||||
type NotificationSetting,
|
type NotificationSetting,
|
||||||
type NotificationStatus,
|
type NotificationStatus,
|
||||||
type NotificationTemplate,
|
type NotificationTemplate,
|
||||||
type PushSubscription,
|
|
||||||
type NotificationType,
|
type NotificationType,
|
||||||
|
type PushSubscription,
|
||||||
type PushSubscriptionKeys
|
type PushSubscriptionKeys
|
||||||
} from '@hcengineering/notification'
|
} from '@hcengineering/notification'
|
||||||
import { getEmbeddedLabel, type Asset, type IntlString, type Resource } from '@hcengineering/platform'
|
import { getEmbeddedLabel, type Asset, type IntlString, type Resource } from '@hcengineering/platform'
|
||||||
@ -91,6 +92,8 @@ export const DOMAIN_NOTIFICATION = 'notification' as Domain
|
|||||||
|
|
||||||
@Model(notification.class.BrowserNotification, core.class.Doc, DOMAIN_NOTIFICATION)
|
@Model(notification.class.BrowserNotification, core.class.Doc, DOMAIN_NOTIFICATION)
|
||||||
export class TBrowserNotification extends TDoc implements BrowserNotification {
|
export class TBrowserNotification extends TDoc implements BrowserNotification {
|
||||||
|
senderId?: Ref<Account> | undefined
|
||||||
|
tag!: Ref<Doc<Space>>
|
||||||
title!: string
|
title!: string
|
||||||
body!: string
|
body!: string
|
||||||
onClickLocation?: Location | undefined
|
onClickLocation?: Location | undefined
|
||||||
@ -365,8 +368,7 @@ export function createModel (builder: Builder): void {
|
|||||||
core.space.Model,
|
core.space.Model,
|
||||||
{
|
{
|
||||||
label: notification.string.Push,
|
label: notification.string.Push,
|
||||||
depends: notification.providers.PlatformNotification,
|
depends: notification.providers.PlatformNotification
|
||||||
onChange: notification.function.CheckPushPermission
|
|
||||||
},
|
},
|
||||||
notification.providers.BrowserNotification
|
notification.providers.BrowserNotification
|
||||||
)
|
)
|
||||||
|
@ -85,14 +85,15 @@ export function addNotification (
|
|||||||
title: string,
|
title: string,
|
||||||
subTitle: string,
|
subTitle: string,
|
||||||
component: AnyComponent | AnySvelteComponent,
|
component: AnyComponent | AnySvelteComponent,
|
||||||
params?: Record<string, any>
|
params?: Record<string, any>,
|
||||||
|
severity: NotificationSeverity = NotificationSeverity.Success
|
||||||
): void {
|
): void {
|
||||||
const closeTimeout = parseInt(localStorage.getItem('#platform.notification.timeout') ?? '10000')
|
const closeTimeout = parseInt(localStorage.getItem('#platform.notification.timeout') ?? '10000')
|
||||||
const notification: Notification = {
|
const notification: Notification = {
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
title,
|
title,
|
||||||
subTitle,
|
subTitle,
|
||||||
severity: NotificationSeverity.Success,
|
severity,
|
||||||
position: NotificationPosition.BottomRight,
|
position: NotificationPosition.BottomRight,
|
||||||
component,
|
component,
|
||||||
closeTimeout,
|
closeTimeout,
|
||||||
|
@ -46,6 +46,8 @@
|
|||||||
"UnstarDocument": "Unstar document",
|
"UnstarDocument": "Unstar document",
|
||||||
"Unsubscribe": "Unsubscribe",
|
"Unsubscribe": "Unsubscribe",
|
||||||
"Push": "Push",
|
"Push": "Push",
|
||||||
"Unreads": "Unreads"
|
"Unreads": "Unreads",
|
||||||
|
"EnablePush": "Enable push notifications",
|
||||||
|
"NotificationBlockedInBrowser": "Notifications are blocked in your browser. Please enable notifications in your browser settings"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,8 @@
|
|||||||
"ArchiveAllConfirmationMessage": "¿Estás seguro de que quieres archivar todas las notificaciones? Esta operación no se puede deshacer.",
|
"ArchiveAllConfirmationMessage": "¿Estás seguro de que quieres archivar todas las notificaciones? Esta operación no se puede deshacer.",
|
||||||
"StarDocument": "Marcar documento",
|
"StarDocument": "Marcar documento",
|
||||||
"UnstarDocument": "Desmarcar documento",
|
"UnstarDocument": "Desmarcar documento",
|
||||||
"Push": "Push"
|
"Push": "Push",
|
||||||
|
"EnablePush": "Habilitar notificaciones push",
|
||||||
|
"NotificationBlockedInBrowser": "Las notificaciones están bloqueadas en tu navegador. Por favor, habilita las notificaciones en la configuración de tu navegador."
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -45,6 +45,8 @@
|
|||||||
"ArchiveAllConfirmationMessage": "Esta seguro que quer arquivar todas as notificações? Esta operação não se pode desfazer.",
|
"ArchiveAllConfirmationMessage": "Esta seguro que quer arquivar todas as notificações? Esta operação não se pode desfazer.",
|
||||||
"StarDocument": "Marcar documento",
|
"StarDocument": "Marcar documento",
|
||||||
"UnstarDocument": "Desmarcar documento",
|
"UnstarDocument": "Desmarcar documento",
|
||||||
"Push": "Push"
|
"Push": "Push",
|
||||||
|
"EnablePush": "Ativar notificações push",
|
||||||
|
"NotificationBlockedInBrowser": "Notificações bloqueadas no navegador. Por favor habilite las notificaciones en la configuración de su navegador."
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -46,6 +46,8 @@
|
|||||||
"UnstarDocument": "Удалить из избранного",
|
"UnstarDocument": "Удалить из избранного",
|
||||||
"Unsubscribe": "Отписаться",
|
"Unsubscribe": "Отписаться",
|
||||||
"Push": "Push",
|
"Push": "Push",
|
||||||
"Unreads": "Непрочитанные"
|
"Unreads": "Непрочитанные",
|
||||||
|
"EnablePush": "Включить Push-уведомления",
|
||||||
|
"NotificationBlockedInBrowser": "Уведомления заблокированы в вашем браузере. Пожалуйста, включите уведомления в настройках браузера"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,76 +14,50 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getCurrentAccount } from '@hcengineering/core'
|
import { getCurrentAccount } from '@hcengineering/core'
|
||||||
import notification from '@hcengineering/notification'
|
import notification, { BrowserNotification, NotificationStatus } from '@hcengineering/notification'
|
||||||
import { getMetadata } from '@hcengineering/platform'
|
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||||
import { getClient } from '@hcengineering/presentation'
|
import { checkPermission, pushAllowed, subscribePush } from '../utils'
|
||||||
import { getCurrentLocation, navigate, parseLocation } from '@hcengineering/ui'
|
import { NotificationSeverity, addNotification } from '@hcengineering/ui'
|
||||||
|
import Notification from './Notification.svelte'
|
||||||
|
|
||||||
|
async function check (allowed: boolean) {
|
||||||
|
if (allowed) {
|
||||||
|
query.unsubscribe()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const res = await checkPermission(true)
|
||||||
|
if (res) {
|
||||||
|
query.unsubscribe()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const isSubscribed = await subscribePush()
|
||||||
|
if (isSubscribed) {
|
||||||
|
query.unsubscribe()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
query.query(
|
||||||
|
notification.class.BrowserNotification,
|
||||||
|
{
|
||||||
|
user: getCurrentAccount()._id,
|
||||||
|
status: NotificationStatus.New,
|
||||||
|
createdOn: { $gt: Date.now() }
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
if (res.length > 0) {
|
||||||
|
notify(res[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
|
|
||||||
const publicKey = getMetadata(notification.metadata.PushPublicKey)
|
async function notify (value: BrowserNotification): Promise<void> {
|
||||||
|
addNotification(value.title, value.body, Notification, { value }, NotificationSeverity.Info)
|
||||||
async function subscribe (): Promise<void> {
|
await client.update(value, { status: NotificationStatus.Notified })
|
||||||
if ('serviceWorker' in navigator && 'PushManager' in window && publicKey !== undefined) {
|
|
||||||
try {
|
|
||||||
const loc = getCurrentLocation()
|
|
||||||
const registration = await navigator.serviceWorker.register('/serviceWorker.js', {
|
|
||||||
scope: `${loc.path[0]}/${loc.path[1]}`
|
|
||||||
})
|
|
||||||
const current = await registration.pushManager.getSubscription()
|
|
||||||
if (current == null) {
|
|
||||||
const subscription = await registration.pushManager.subscribe({
|
|
||||||
userVisibleOnly: true,
|
|
||||||
applicationServerKey: publicKey
|
|
||||||
})
|
|
||||||
await client.createDoc(notification.class.PushSubscription, notification.space.Notifications, {
|
|
||||||
user: getCurrentAccount()._id,
|
|
||||||
endpoint: subscription.endpoint,
|
|
||||||
keys: {
|
|
||||||
p256dh: arrayBufferToBase64(subscription.getKey('p256dh')),
|
|
||||||
auth: arrayBufferToBase64(subscription.getKey('auth'))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const exists = await client.findOne(notification.class.PushSubscription, {
|
|
||||||
user: getCurrentAccount()._id,
|
|
||||||
endpoint: current.endpoint
|
|
||||||
})
|
|
||||||
if (exists === undefined) {
|
|
||||||
await client.createDoc(notification.class.PushSubscription, notification.space.Notifications, {
|
|
||||||
user: getCurrentAccount()._id,
|
|
||||||
endpoint: current.endpoint,
|
|
||||||
keys: {
|
|
||||||
p256dh: arrayBufferToBase64(current.getKey('p256dh')),
|
|
||||||
auth: arrayBufferToBase64(current.getKey('auth'))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
navigator.serviceWorker.addEventListener('message', (event) => {
|
|
||||||
if (event.data && event.data.type === 'notification-click') {
|
|
||||||
const { url } = event.data
|
|
||||||
if (url !== undefined) {
|
|
||||||
navigate(parseLocation(new URL(url)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Service Worker registration failed:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function arrayBufferToBase64 (buffer: ArrayBuffer | null): string {
|
const query = createQuery()
|
||||||
if (buffer) {
|
|
||||||
const bytes = new Uint8Array(buffer)
|
|
||||||
const array = Array.from(bytes)
|
|
||||||
const binary = String.fromCharCode.apply(null, array)
|
|
||||||
return btoa(binary)
|
|
||||||
} else {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribe()
|
$: check($pushAllowed)
|
||||||
</script>
|
</script>
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { PersonAccount } from '@hcengineering/contact'
|
||||||
|
import { Avatar, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
|
||||||
|
import { Ref } from '@hcengineering/core'
|
||||||
|
import { BrowserNotification } from '@hcengineering/notification'
|
||||||
|
import { Button, Notification as PlatformNotification, NotificationToast, navigate } from '@hcengineering/ui'
|
||||||
|
import view from '@hcengineering/view'
|
||||||
|
import { pushAvailable, subscribePush } from '../utils'
|
||||||
|
import plugin from '../plugin'
|
||||||
|
|
||||||
|
export let notification: PlatformNotification
|
||||||
|
export let onRemove: () => void
|
||||||
|
|
||||||
|
$: value = notification.params?.value as BrowserNotification
|
||||||
|
|
||||||
|
$: senderAccount =
|
||||||
|
value.senderId !== undefined ? $personAccountByIdStore.get(value.senderId as Ref<PersonAccount>) : undefined
|
||||||
|
$: sender = senderAccount !== undefined ? $personByIdStore.get(senderAccount.person) : undefined
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<NotificationToast title={notification.title} severity={notification.severity} onClose={onRemove}>
|
||||||
|
<svelte:fragment slot="content">
|
||||||
|
<div class="flex-row-center flex-wrap gap-2">
|
||||||
|
{#if sender}
|
||||||
|
<Avatar avatar={sender.avatar} name={sender.name} size={'small'} />
|
||||||
|
{/if}
|
||||||
|
<span class="overflow-label">
|
||||||
|
{value.body}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</svelte:fragment>
|
||||||
|
|
||||||
|
<svelte:fragment slot="buttons">
|
||||||
|
{#if value.onClickLocation}
|
||||||
|
<Button
|
||||||
|
label={view.string.Open}
|
||||||
|
on:click={() => {
|
||||||
|
if (value.onClickLocation) {
|
||||||
|
onRemove()
|
||||||
|
navigate(value.onClickLocation)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<Button
|
||||||
|
label={plugin.string.EnablePush}
|
||||||
|
disabled={!pushAvailable()}
|
||||||
|
showTooltip={!pushAvailable() ? { label: plugin.string.NotificationBlockedInBrowser } : undefined}
|
||||||
|
on:click={subscribePush}
|
||||||
|
/>
|
||||||
|
</svelte:fragment>
|
||||||
|
</NotificationToast>
|
@ -35,6 +35,8 @@ export default mergeIds(notificationId, notification, {
|
|||||||
People: '' as IntlString,
|
People: '' as IntlString,
|
||||||
Read: '' as IntlString,
|
Read: '' as IntlString,
|
||||||
Unread: '' as IntlString,
|
Unread: '' as IntlString,
|
||||||
Unreads: '' as IntlString
|
Unreads: '' as IntlString,
|
||||||
|
EnablePush: '' as IntlString,
|
||||||
|
NotificationBlockedInBrowser: '' as IntlString
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -30,6 +30,7 @@ import {
|
|||||||
type WithLookup
|
type WithLookup
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import notification, {
|
import notification, {
|
||||||
|
NotificationStatus,
|
||||||
decodeObjectURI,
|
decodeObjectURI,
|
||||||
encodeObjectURI,
|
encodeObjectURI,
|
||||||
notificationId,
|
notificationId,
|
||||||
@ -45,14 +46,16 @@ import {
|
|||||||
getCurrentLocation,
|
getCurrentLocation,
|
||||||
getLocation,
|
getLocation,
|
||||||
navigate,
|
navigate,
|
||||||
|
parseLocation,
|
||||||
showPopup,
|
showPopup,
|
||||||
type Location,
|
type Location,
|
||||||
type ResolvedLocation
|
type ResolvedLocation
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import { get } from 'svelte/store'
|
import { get, writable } from 'svelte/store'
|
||||||
|
|
||||||
import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
|
import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
|
||||||
import { type InboxData, type InboxNotificationsFilter } from './types'
|
import { type InboxData, type InboxNotificationsFilter } from './types'
|
||||||
|
import { getMetadata } from '@hcengineering/platform'
|
||||||
|
|
||||||
export async function hasDocNotifyContextPinAction (docNotifyContext: DocNotifyContext): Promise<boolean> {
|
export async function hasDocNotifyContextPinAction (docNotifyContext: DocNotifyContext): Promise<boolean> {
|
||||||
if (docNotifyContext.hidden) {
|
if (docNotifyContext.hidden) {
|
||||||
@ -540,6 +543,8 @@ export function openInboxDoc (
|
|||||||
navigate(loc)
|
navigate(loc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const pushAllowed = writable<boolean>(false)
|
||||||
|
|
||||||
export async function checkPermission (value: boolean): Promise<boolean> {
|
export async function checkPermission (value: boolean): Promise<boolean> {
|
||||||
if (!value) return true
|
if (!value) return true
|
||||||
if ('serviceWorker' in navigator && 'PushManager' in window) {
|
if ('serviceWorker' in navigator && 'PushManager' in window) {
|
||||||
@ -548,32 +553,121 @@ export async function checkPermission (value: boolean): Promise<boolean> {
|
|||||||
const registration = await navigator.serviceWorker.getRegistration(`/${loc.path[0]}/${loc.path[1]}`)
|
const registration = await navigator.serviceWorker.getRegistration(`/${loc.path[0]}/${loc.path[1]}`)
|
||||||
if (registration !== undefined) {
|
if (registration !== undefined) {
|
||||||
const current = await registration.pushManager.getSubscription()
|
const current = await registration.pushManager.getSubscription()
|
||||||
return current !== null
|
const res = current !== null
|
||||||
|
pushAllowed.set(current !== null)
|
||||||
|
void registration.update()
|
||||||
|
addWorkerListener()
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
pushAllowed.set(false)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pushAllowed.set(false)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function askPermission (): Promise<void> {
|
function addWorkerListener (): void {
|
||||||
if ('Notification' in window && Notification?.permission === 'default') {
|
navigator.serviceWorker.addEventListener('message', (event) => {
|
||||||
await Notification?.requestPermission()
|
if (event.data !== undefined && event.data.type === 'notification-click') {
|
||||||
|
const { url, _id } = event.data
|
||||||
|
if (url !== undefined) {
|
||||||
|
navigate(parseLocation(new URL(url)))
|
||||||
|
}
|
||||||
|
if (_id !== undefined) {
|
||||||
|
void cleanTag(_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pushAvailable (): boolean {
|
||||||
|
const publicKey = getMetadata(notification.metadata.PushPublicKey)
|
||||||
|
return (
|
||||||
|
'serviceWorker' in navigator &&
|
||||||
|
'PushManager' in window &&
|
||||||
|
publicKey !== undefined &&
|
||||||
|
'Notification' in window &&
|
||||||
|
Notification.permission !== 'denied'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function subscribePush (): Promise<boolean> {
|
||||||
|
const client = getClient()
|
||||||
|
const publicKey = getMetadata(notification.metadata.PushPublicKey)
|
||||||
|
if ('serviceWorker' in navigator && 'PushManager' in window && publicKey !== undefined) {
|
||||||
|
try {
|
||||||
|
const loc = getCurrentLocation()
|
||||||
|
let registration = await navigator.serviceWorker.getRegistration(`/${loc.path[0]}/${loc.path[1]}`)
|
||||||
|
if (registration !== undefined) {
|
||||||
|
await registration.update()
|
||||||
|
} else {
|
||||||
|
registration = await navigator.serviceWorker.register('/serviceWorker.js', {
|
||||||
|
scope: `/${loc.path[0]}/${loc.path[1]}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const current = await registration.pushManager.getSubscription()
|
||||||
|
if (current == null) {
|
||||||
|
const subscription = await registration.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: publicKey
|
||||||
|
})
|
||||||
|
await client.createDoc(notification.class.PushSubscription, notification.space.Notifications, {
|
||||||
|
user: getCurrentAccount()._id,
|
||||||
|
endpoint: subscription.endpoint,
|
||||||
|
keys: {
|
||||||
|
p256dh: arrayBufferToBase64(subscription.getKey('p256dh')),
|
||||||
|
auth: arrayBufferToBase64(subscription.getKey('auth'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const exists = await client.findOne(notification.class.PushSubscription, {
|
||||||
|
user: getCurrentAccount()._id,
|
||||||
|
endpoint: current.endpoint
|
||||||
|
})
|
||||||
|
if (exists === undefined) {
|
||||||
|
await client.createDoc(notification.class.PushSubscription, notification.space.Notifications, {
|
||||||
|
user: getCurrentAccount()._id,
|
||||||
|
endpoint: current.endpoint,
|
||||||
|
keys: {
|
||||||
|
p256dh: arrayBufferToBase64(current.getKey('p256dh')),
|
||||||
|
auth: arrayBufferToBase64(current.getKey('auth'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addWorkerListener()
|
||||||
|
pushAllowed.set(true)
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Service Worker registration failed:', err)
|
||||||
|
pushAllowed.set(false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pushAllowed.set(false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanTag (_id: Ref<Doc>): Promise<void> {
|
||||||
|
const client = getClient()
|
||||||
|
const notifications = await client.findAll(notification.class.BrowserNotification, {
|
||||||
|
tag: _id,
|
||||||
|
status: NotificationStatus.New
|
||||||
|
})
|
||||||
|
for (const notification of notifications) {
|
||||||
|
await client.update(notification, { status: NotificationStatus.Notified })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function notify (title: string, body: string, _id?: string, onClick?: () => void): void {
|
function arrayBufferToBase64 (buffer: ArrayBuffer | null): string {
|
||||||
if ('Notification' in window && Notification?.permission === 'granted') {
|
if (buffer != null) {
|
||||||
const req: NotificationOptions = {
|
const bytes = new Uint8Array(buffer)
|
||||||
body
|
const array = Array.from(bytes)
|
||||||
}
|
const binary = String.fromCharCode.apply(null, array)
|
||||||
if (_id !== undefined) {
|
return btoa(binary)
|
||||||
req.tag = _id
|
} else {
|
||||||
}
|
return ''
|
||||||
const notification = new Notification(title, req)
|
|
||||||
if (onClick !== undefined) {
|
|
||||||
notification.onclick = onClick
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,8 @@ export interface BrowserNotification extends Doc {
|
|||||||
title: string
|
title: string
|
||||||
body: string
|
body: string
|
||||||
onClickLocation?: Location
|
onClickLocation?: Location
|
||||||
|
senderId?: Ref<Account>
|
||||||
|
tag: Ref<Doc>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PushData {
|
export interface PushData {
|
||||||
|
@ -18,41 +18,56 @@ self.addEventListener('push', (event: PushEvent) => {
|
|||||||
tag: payload.tag,
|
tag: payload.tag,
|
||||||
data: {
|
data: {
|
||||||
domain: payload.domain,
|
domain: payload.domain,
|
||||||
url: payload.url
|
url: payload.url,
|
||||||
|
notificationId: payload.tag
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Listen for notification click event
|
async function handleNotificationClick (event: any): Promise<void> {
|
||||||
self.addEventListener('notificationclick', (event: any) => {
|
event.notification.close()
|
||||||
const clickedNotification = event.notification
|
const clickedNotification = event.notification
|
||||||
const notificationData = clickedNotification.data
|
const notificationData = clickedNotification.data
|
||||||
|
const notificationId = notificationData.notificationId
|
||||||
const notificationUrl = notificationData.url
|
const notificationUrl = notificationData.url
|
||||||
const domain = notificationData.domain
|
const domain = notificationData.domain
|
||||||
|
|
||||||
if (notificationUrl !== undefined && domain !== undefined) {
|
if (notificationUrl !== undefined && domain !== undefined) {
|
||||||
// Check if any client with the same origin is already open
|
const windowClients = (await self.clients.matchAll({
|
||||||
event.waitUntil(
|
type: 'window',
|
||||||
// Check all active clients (browser windows or tabs)
|
includeUncontrolled: true
|
||||||
self.clients
|
})) as ReadonlyArray<any>
|
||||||
.matchAll({
|
|
||||||
type: 'window',
|
const targetUrl = new URL(notificationUrl)
|
||||||
includeUncontrolled: true
|
for (const client of windowClients) {
|
||||||
|
const clientUrl = new URL(client.url, self.location.href)
|
||||||
|
if (decodeURI(clientUrl.pathname) === targetUrl.pathname) {
|
||||||
|
client.postMessage({
|
||||||
|
type: 'notification-click',
|
||||||
|
url: notificationUrl,
|
||||||
|
_id: notificationId
|
||||||
})
|
})
|
||||||
.then((clientList: any) => {
|
await client.focus()
|
||||||
// Loop through each client
|
return
|
||||||
for (const client of clientList) {
|
}
|
||||||
// If a client has the same URL origin, focus and navigate to it
|
}
|
||||||
if ((client.url as string)?.startsWith(domain)) {
|
|
||||||
client.postMessage({
|
for (const client of windowClients) {
|
||||||
type: 'notification-click',
|
if ((client.url as string)?.startsWith(domain)) {
|
||||||
url: notificationUrl
|
client.postMessage({
|
||||||
})
|
type: 'notification-click',
|
||||||
return client.focus()
|
url: notificationUrl,
|
||||||
}
|
_id: notificationId
|
||||||
}
|
|
||||||
// If no client with the same URL origin is found, open a new window/tab
|
|
||||||
return self.clients.openWindow(notificationUrl)
|
|
||||||
})
|
})
|
||||||
)
|
await client.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('No matching client found')
|
||||||
|
// If no client with the same URL origin is found, open a new window/tab
|
||||||
|
await self.clients.openWindow(notificationUrl)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
self.addEventListener('notificationclick', (e: any) => e.waitUntil(handleNotificationClick(e)))
|
||||||
|
@ -61,6 +61,8 @@ import notification, {
|
|||||||
InboxNotification,
|
InboxNotification,
|
||||||
MentionInboxNotification,
|
MentionInboxNotification,
|
||||||
notificationId,
|
notificationId,
|
||||||
|
NotificationStatus,
|
||||||
|
PushSubscription,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
PushData
|
PushData
|
||||||
} from '@hcengineering/notification'
|
} from '@hcengineering/notification'
|
||||||
@ -424,7 +426,8 @@ export async function pushInboxNotifications (
|
|||||||
const notificationTx = control.txFactory.createTxCreateDoc(_class, space, notificationData)
|
const notificationTx = control.txFactory.createTxCreateDoc(_class, space, notificationData)
|
||||||
res.push(notificationTx)
|
res.push(notificationTx)
|
||||||
if (shouldPush) {
|
if (shouldPush) {
|
||||||
await createPushFromInbox(
|
const now = Date.now()
|
||||||
|
const pushTx = await createPushFromInbox(
|
||||||
control,
|
control,
|
||||||
targetUser,
|
targetUser,
|
||||||
attachedTo,
|
attachedTo,
|
||||||
@ -434,6 +437,10 @@ export async function pushInboxNotifications (
|
|||||||
senderId,
|
senderId,
|
||||||
notificationTx.objectId
|
notificationTx.objectId
|
||||||
)
|
)
|
||||||
|
console.log('Push takes', Date.now() - now, 'ms')
|
||||||
|
if (pushTx !== undefined) {
|
||||||
|
res.push(pushTx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -522,8 +529,8 @@ export async function createPushFromInbox (
|
|||||||
data: Data<InboxNotification>,
|
data: Data<InboxNotification>,
|
||||||
_class: Ref<Class<InboxNotification>>,
|
_class: Ref<Class<InboxNotification>>,
|
||||||
senderId: Ref<PersonAccount>,
|
senderId: Ref<PersonAccount>,
|
||||||
_id: string
|
_id: Ref<Doc>
|
||||||
): Promise<void> {
|
): Promise<Tx | undefined> {
|
||||||
let title: string = ''
|
let title: string = ''
|
||||||
let body: string = ''
|
let body: string = ''
|
||||||
if (control.hierarchy.isDerived(_class, notification.class.ActivityInboxNotification)) {
|
if (control.hierarchy.isDerived(_class, notification.class.ActivityInboxNotification)) {
|
||||||
@ -543,10 +550,24 @@ export async function createPushFromInbox (
|
|||||||
if (sender !== undefined) {
|
if (sender !== undefined) {
|
||||||
senderPerson = (await control.findAll(contact.class.Person, { _id: sender.person }))[0]
|
senderPerson = (await control.findAll(contact.class.Person, { _id: sender.person }))[0]
|
||||||
}
|
}
|
||||||
await createPushNotification(control, targetUser, title, body, _id, senderPerson?.avatar, [
|
const path = [
|
||||||
|
workbenchId,
|
||||||
|
control.workspace.workspaceUrl,
|
||||||
notificationId,
|
notificationId,
|
||||||
encodeObjectURI(attachedTo, attachedToClass)
|
encodeObjectURI(attachedTo, attachedToClass)
|
||||||
])
|
]
|
||||||
|
await createPushNotification(control, targetUser, title, body, _id, senderPerson?.avatar, path)
|
||||||
|
return control.txFactory.createTxCreateDoc(notification.class.BrowserNotification, notification.space.Notifications, {
|
||||||
|
user: targetUser,
|
||||||
|
status: NotificationStatus.New,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
senderId,
|
||||||
|
tag: _id,
|
||||||
|
onClickLocation: {
|
||||||
|
path
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createPushNotification (
|
export async function createPushNotification (
|
||||||
@ -556,7 +577,7 @@ export async function createPushNotification (
|
|||||||
body: string,
|
body: string,
|
||||||
_id: string,
|
_id: string,
|
||||||
senderAvatar?: string | null,
|
senderAvatar?: string | null,
|
||||||
subPath?: string[]
|
path?: string[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const publicKey = getMetadata(notification.metadata.PushPublicKey)
|
const publicKey = getMetadata(notification.metadata.PushPublicKey)
|
||||||
const privateKey = getMetadata(serverNotification.metadata.PushPrivateKey)
|
const privateKey = getMetadata(serverNotification.metadata.PushPrivateKey)
|
||||||
@ -577,9 +598,8 @@ export async function createPushNotification (
|
|||||||
const domainPath = `${workbenchId}/${control.workspace.workspaceUrl}`
|
const domainPath = `${workbenchId}/${control.workspace.workspaceUrl}`
|
||||||
const domain = concatLink(front, domainPath)
|
const domain = concatLink(front, domainPath)
|
||||||
data.domain = domain
|
data.domain = domain
|
||||||
if (subPath !== undefined) {
|
if (path !== undefined) {
|
||||||
const path = [domainPath, ...subPath].join('/')
|
const url = concatLink(front, path.join('/'))
|
||||||
const url = concatLink(front, path)
|
|
||||||
data.url = url
|
data.url = url
|
||||||
}
|
}
|
||||||
if (senderAvatar != null) {
|
if (senderAvatar != null) {
|
||||||
@ -598,14 +618,23 @@ export async function createPushNotification (
|
|||||||
webpush.setVapidDetails(subject, publicKey, privateKey)
|
webpush.setVapidDetails(subject, publicKey, privateKey)
|
||||||
|
|
||||||
for (const subscription of subscriptions) {
|
for (const subscription of subscriptions) {
|
||||||
try {
|
void sendPushToSubscription(control, targetUser, subscription, data)
|
||||||
await webpush.sendNotification(subscription, JSON.stringify(data))
|
}
|
||||||
} catch (err) {
|
}
|
||||||
console.log('Cannot send push notification to', targetUser, err)
|
|
||||||
if (err instanceof WebPushError && err.body.includes('expired')) {
|
async function sendPushToSubscription (
|
||||||
const tx = control.txFactory.createTxRemoveDoc(subscription._class, subscription.space, subscription._id)
|
control: TriggerControl,
|
||||||
await control.apply([tx], true)
|
targetUser: Ref<Account>,
|
||||||
}
|
subscription: PushSubscription,
|
||||||
|
data: PushData
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await webpush.sendNotification(subscription, JSON.stringify(data))
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Cannot send push notification to', targetUser, err)
|
||||||
|
if (err instanceof WebPushError && err.body.includes('expired')) {
|
||||||
|
const tx = control.txFactory.createTxRemoveDoc(subscription._class, subscription.space, subscription._id)
|
||||||
|
await control.apply([tx], true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,8 +97,7 @@ export async function getIssueNotificationContent (
|
|||||||
): Promise<NotificationContent> {
|
): Promise<NotificationContent> {
|
||||||
const issue = doc as Issue
|
const issue = doc as Issue
|
||||||
|
|
||||||
const issueShortName = await issueTextPresenter(doc)
|
const issueTitle = await issueTextPresenter(doc)
|
||||||
const issueTitle = `${issueShortName}: ${issue.title}`
|
|
||||||
|
|
||||||
const title = tracker.string.IssueNotificationTitle
|
const title = tracker.string.IssueNotificationTitle
|
||||||
let body = tracker.string.IssueNotificationBody
|
let body = tracker.string.IssueNotificationBody
|
||||||
|
Loading…
Reference in New Issue
Block a user