Add browser notifications sound (#8515)

Signed-off-by: Anton Alexeyev <alexeyev.anton@gmail.com>
This commit is contained in:
utkaka 2025-04-10 21:47:48 +07:00 committed by GitHub
parent 601fabd712
commit afbaf26078
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 77 additions and 27 deletions

View File

@ -159,6 +159,17 @@ export function defineNotifications (builder: Builder): void {
]
})
builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, {
provider: notification.providers.SoundNotificationProvider,
ignoredTypes: [],
enabledTypes: [
chunter.ids.DMNotification,
chunter.ids.ChannelNotification,
chunter.ids.ThreadNotification,
chunter.ids.JoinChannelNotification
]
})
builder.createDoc(notification.class.ActivityNotificationViewlet, core.space.Model, {
messageMatch: {
_class: activity.class.DocUpdateMessage,

View File

@ -106,6 +106,7 @@ export class TBrowserNotification extends TDoc implements BrowserNotification {
messageClass?: Ref<Class<ActivityMessage>>
objectId!: Ref<Doc>
objectClass!: Ref<Class<Doc>>
soundAlert!: boolean
}
@Model(notification.class.PushSubscription, core.class.Doc, DOMAIN_USER_NOTIFY)
@ -776,7 +777,6 @@ export function createModel (builder: Builder): void {
depends: notification.providers.PushNotificationProvider,
defaultEnabled: true,
canDisable: true,
ignoreAll: true,
order: 250
},
notification.providers.SoundNotificationProvider
@ -787,6 +787,12 @@ export function createModel (builder: Builder): void {
ignoredTypes: [notification.ids.CollaboratoAddNotification],
enabledTypes: []
})
builder.createDoc(notification.class.NotificationProviderDefaults, core.space.Model, {
provider: notification.providers.SoundNotificationProvider,
ignoredTypes: [notification.ids.CollaboratoAddNotification],
enabledTypes: []
})
}
export function generateClassNotificationTypes (

View File

@ -6,21 +6,20 @@ import notification from '@hcengineering/notification'
const sounds = new Map<Asset, AudioBuffer>()
const context = new AudioContext()
export async function prepareSound (key: string, _class?: Ref<Class<Doc>>): Promise<void> {
if (_class === undefined) return
export async function isNotificationAllowed (_class?: Ref<Class<Doc>>): Promise<boolean> {
if (_class === undefined) return false
const client = getClient()
const notificationType = client
.getModel()
.findAllSync(notification.class.NotificationType, { objectClass: _class })[0]
if (notificationType === undefined) return
if (notificationType === undefined) return false
const isAllowedFn = await getResource(notification.function.IsNotificationAllowed)
const allowed: boolean = isAllowedFn(notificationType, notification.providers.SoundNotificationProvider)
if (!allowed) return
return isAllowedFn(notificationType, notification.providers.SoundNotificationProvider)
}
export async function prepareSound (key: string): Promise<void> {
try {
const soundUrl = getMetadata(key as Asset) as string
const rawAudio = await fetch(soundUrl)
@ -33,14 +32,11 @@ export async function prepareSound (key: string, _class?: Ref<Class<Doc>>): Prom
}
}
export async function playSound (
soundKey: string,
_class?: Ref<Class<Doc>>,
loop = false
): Promise<(() => void) | null> {
export async function playSound (soundKey: string, loop = false): Promise<(() => void) | null> {
const soundAssetKey = soundKey as Asset
if (!sounds.has(soundAssetKey)) {
await prepareSound(soundKey, _class)
await prepareSound(soundKey)
}
const sound = sounds.get(soundKey as Asset)
@ -65,3 +61,13 @@ export async function playSound (
return null
}
}
export async function playNotificationSound (
soundKey: string,
_class?: Ref<Class<Doc>>,
loop = false
): Promise<(() => void) | null> {
const allowed = await isNotificationAllowed(_class)
if (!allowed) return null
return await playSound(soundKey, loop)
}

View File

@ -15,18 +15,20 @@
<script lang="ts">
import { formatName } from '@hcengineering/contact'
import { Avatar, personByIdStore } from '@hcengineering/contact-resources'
import { getClient } from '@hcengineering/presentation'
import { getClient, playNotificationSound } from '@hcengineering/presentation'
import { Button, Label } from '@hcengineering/ui'
import { Invite, RequestStatus, getFreeRoomPlace } from '@hcengineering/love'
import love from '../plugin'
import { infos, myInfo, rooms } from '../stores'
import { connectRoom } from '../utils'
import { onDestroy, onMount } from 'svelte'
export let invite: Invite
$: person = $personByIdStore.get(invite.from)
const client = getClient()
let stopSound: (() => void) | null = null
async function accept (): Promise<void> {
const room = $rooms.find((p) => p._id === invite.room)
@ -45,6 +47,14 @@
async function decline (): Promise<void> {
await client.update(invite, { status: RequestStatus.Rejected })
}
onMount(async () => {
stopSound = await playNotificationSound(love.sound.Knock, love.class.Invite, true)
})
onDestroy(() => {
stopSound?.()
})
</script>
<div class="antiPopup flex-col-center">

View File

@ -15,7 +15,7 @@
<script lang="ts">
import { formatName, getCurrentEmployee } from '@hcengineering/contact'
import { Avatar, personByIdStore } from '@hcengineering/contact-resources'
import { getClient, playSound } from '@hcengineering/presentation'
import { getClient, playNotificationSound } from '@hcengineering/presentation'
import { Button, Label } from '@hcengineering/ui'
import { JoinRequest, RequestStatus } from '@hcengineering/love'
import love from '../plugin'
@ -45,7 +45,7 @@
}
onMount(async () => {
stopSound = await playSound(love.sound.Knock, love.class.JoinRequest, true)
stopSound = await playNotificationSound(love.sound.Knock, love.class.JoinRequest, true)
})
onDestroy(() => {

View File

@ -22,3 +22,6 @@ loadMetadata(notification.icon, {
Inbox: `${icons}#inbox`,
BellCrossed: `${icons}#bell-crossed`
})
loadMetadata(notification.sound, {
InboxNotification: require('../assets/inbox-notification.wav')
})

View File

@ -7,10 +7,10 @@
import chunter, { ThreadMessage } from '@hcengineering/chunter'
import { getResource } from '@hcengineering/platform'
import activity, { ActivityMessage } from '@hcengineering/activity'
import { getClient } from '@hcengineering/presentation'
import { getClient, playSound } from '@hcengineering/presentation'
import { pushAvailable, subscribePush } from '../utils'
import plugin from '../plugin'
import { onMount } from 'svelte'
export let notification: PlatformNotification
export let onRemove: () => void
@ -54,6 +54,11 @@
const fn = await getResource(chunter.function.OpenChannelInSidebar)
await fn(_id, _class, undefined, thread, true, selectedMessageId)
}
onMount(async () => {
if (!value.soundAlert) return
await playSound(plugin.sound.InboxNotification)
})
</script>
<NotificationToast title={notification.title} severity={notification.severity} onClose={onRemove}>

View File

@ -62,6 +62,7 @@ export interface BrowserNotification extends Doc {
messageClass?: Ref<Class<ActivityMessage>>
objectId: Ref<Doc>
objectClass: Ref<Class<Doc>>
soundAlert: boolean
}
export interface PushData {
@ -405,6 +406,9 @@ const notification = plugin(notificationId, {
Inbox: '' as Asset,
BellCrossed: '' as Asset
},
sound: {
InboxNotification: '' as Asset
},
string: {
Notification: '' as IntlString,
Notifications: '' as IntlString,

View File

@ -54,6 +54,7 @@ async function createPushFromInbox (
control: TriggerControl,
n: InboxNotification,
receiver: AccountUuid,
soundAlert: boolean,
receiverSpace: Ref<PersonSpace>,
subscriptions: PushSubscription[],
senderPerson?: Person
@ -87,7 +88,10 @@ async function createPushFromInbox (
}
const path = [workbenchId, control.workspace.url, notificationId, encodeObjectURI(id, n.objectClass)]
await createPushNotification(control, receiver, title, body, n._id, subscriptions, senderPerson, path)
if (subscriptions.length > 0) {
await createPushNotification(control, receiver, title, body, n._id, subscriptions, senderPerson, path)
}
const messageInfo = getMessageInfo(n, control.hierarchy)
return control.txFactory.createTxCreateDoc(notification.class.BrowserNotification, receiverSpace, {
@ -102,7 +106,8 @@ async function createPushFromInbox (
messageClass: messageInfo._class,
onClickLocation: {
path
}
},
soundAlert
})
}
@ -246,23 +251,23 @@ export async function PushNotificationsHandler (
receivers.has(it.user)
)
if (subscriptions.length === 0) {
return []
}
const res: Tx[] = []
for (const inboxNotification of all) {
const { user } = inboxNotification
const userSubscriptions = subscriptions.filter((it) => it.user === user)
if (userSubscriptions.length === 0) continue
const senderSocialString = inboxNotification.createdBy ?? inboxNotification.modifiedBy
const senderPerson = await getPerson(control, senderSocialString)
const soundAlert =
availableProviders
.get(inboxNotification._id)
?.find((p) => p === notification.providers.SoundNotificationProvider) !== undefined
const tx = await createPushFromInbox(
control,
inboxNotification,
user,
soundAlert,
inboxNotification.space,
userSubscriptions,
senderPerson

View File

@ -36,7 +36,7 @@ export class NotificationsPage {
documents = (): Locator => this.page.getByRole('button', { name: 'Documents' })
requests = (): Locator => this.page.getByRole('button', { name: 'Requests' })
todos = (): Locator => this.page.getByRole('button', { name: "Todo's" })
chatMessageToggle = (): Locator => this.page.locator('.grid > div:nth-child(6)')
chatMessageToggle = (): Locator => this.page.locator('.grid > div:nth-child(7)')
constructor (page: Page) {
this.page = page