diff --git a/dev/prod/webpack.config.js b/dev/prod/webpack.config.js index ad3db50282..069ff9da08 100644 --- a/dev/prod/webpack.config.js +++ b/dev/prod/webpack.config.js @@ -211,6 +211,16 @@ module.exports = [ } } }, + { + test: /\.(wav|ogg)$/, + use: { + loader: 'file-loader', + options: { + 'name': 'snd/[contenthash].[ext]', + esModule: false + } + } + }, { test: /\.svg$/, use: [ diff --git a/models/love/src/index.ts b/models/love/src/index.ts index 217fb15cea..0725cf11f2 100644 --- a/models/love/src/index.ts +++ b/models/love/src/index.ts @@ -207,7 +207,8 @@ export function createModel (builder: Builder): void { objectClass: love.class.JoinRequest, providers: { [notification.providers.PlatformNotification]: true, - [notification.providers.BrowserNotification]: true + [notification.providers.BrowserNotification]: true, + [notification.providers.SoundNotification]: true } }, love.ids.KnockNotification diff --git a/models/notification/src/index.ts b/models/notification/src/index.ts index 5fd732b970..59cea8604a 100644 --- a/models/notification/src/index.ts +++ b/models/notification/src/index.ts @@ -350,6 +350,15 @@ export function createModel (builder: Builder): void { notification.providers.BrowserNotification ) + builder.createDoc( + notification.class.NotificationProvider, + core.space.Model, + { + label: notification.string.Sound + }, + notification.providers.SoundNotification + ) + builder.createDoc( notification.class.NotificationProvider, core.space.Model, diff --git a/packages/presentation/package.json b/packages/presentation/package.json index 304fc32b7e..c398bb4f49 100644 --- a/packages/presentation/package.json +++ b/packages/presentation/package.json @@ -41,6 +41,7 @@ "dependencies": { "@hcengineering/platform": "^0.6.11", "@hcengineering/core": "^0.6.32", + "@hcengineering/notification": "^0.6.23", "@hcengineering/analytics": "^0.6.0", "@hcengineering/query": "^0.6.12", "@hcengineering/ui": "^0.6.15", diff --git a/packages/presentation/src/index.ts b/packages/presentation/src/index.ts index c061bd66d8..b280464cc2 100644 --- a/packages/presentation/src/index.ts +++ b/packages/presentation/src/index.ts @@ -62,3 +62,4 @@ export * from './rules' export * from './search' export * from './image' export * from './preview' +export * from './sound' diff --git a/packages/presentation/src/sound.ts b/packages/presentation/src/sound.ts new file mode 100644 index 0000000000..a326086f0a --- /dev/null +++ b/packages/presentation/src/sound.ts @@ -0,0 +1,54 @@ +import { type Class, type Doc, type Ref } from '@hcengineering/core' +import { type Asset, getMetadata } from '@hcengineering/platform' +import { getClient } from '.' +import notification from '@hcengineering/notification' + +const sounds = new Map() +const context = new AudioContext() +export async function prepareSound (key: string, _class?: Ref>, loop = false, play = false): Promise { + const notificationType = + _class !== undefined + ? getClient().getModel().findAllSync(notification.class.NotificationType, { objectClass: _class }) + : undefined + const notAllowed = notificationType?.[0].providers[notification.providers.SoundNotification] === false + if (notificationType === undefined || notAllowed) { + return + } + try { + const soundUrl = getMetadata(key as Asset) as string + const audioBuffer = await fetch(soundUrl) + .then(async (res) => await res.arrayBuffer()) + .then(async (ArrayBuffer) => await context.decodeAudioData(ArrayBuffer)) + const audio = context.createBufferSource() + audio.buffer = audioBuffer + audio.loop = loop + sounds.set(key as Asset, audio) + if (play) { + playSound(key) + } + } catch (err) { + console.error('sound not found', key) + } +} + +export function playSound (soundKey: string, _class?: Ref>, loop = false): void { + const sound = sounds.get(soundKey as Asset) + if (sound !== undefined) { + try { + sound.connect(context.destination) + sound.start() + } catch (err) { + console.error('error happened during sound play', soundKey, err) + } + } else { + void prepareSound(soundKey, _class, loop, true) + } +} + +export function stopSound (soundKey: string): void { + const sound = sounds.get(soundKey as Asset) + if (sound !== undefined && sound?.context.state === 'running') { + sound.stop() + sound.disconnect(context.destination) + } +} diff --git a/plugins/love-assets/assets/knock.wav b/plugins/love-assets/assets/knock.wav new file mode 100644 index 0000000000..8180a2e121 Binary files /dev/null and b/plugins/love-assets/assets/knock.wav differ diff --git a/plugins/love-assets/src/index.ts b/plugins/love-assets/src/index.ts index 266fd9295a..6e1f80e6cf 100644 --- a/plugins/love-assets/src/index.ts +++ b/plugins/love-assets/src/index.ts @@ -38,3 +38,6 @@ loadMetadata(love.icon, { ExitFullScreen: `${icons}#exitfullscreen`, Invite: `${icons}#invite` }) +loadMetadata(love.sound, { + Knock: require('../assets/knock.wav') +}) diff --git a/plugins/love-resources/src/components/InvitePopup.svelte b/plugins/love-resources/src/components/InvitePopup.svelte index d4233c912a..b938bd0d6c 100644 --- a/plugins/love-resources/src/components/InvitePopup.svelte +++ b/plugins/love-resources/src/components/InvitePopup.svelte @@ -41,7 +41,6 @@ ) await connectRoom(place.x, place.y, $myInfo, myPerson, room) } - async function decline (): Promise { await client.update(invite, { status: RequestStatus.Rejected }) } diff --git a/plugins/love-resources/src/components/RequestPopup.svelte b/plugins/love-resources/src/components/RequestPopup.svelte index 52710f720f..23ec1958ef 100644 --- a/plugins/love-resources/src/components/RequestPopup.svelte +++ b/plugins/love-resources/src/components/RequestPopup.svelte @@ -16,12 +16,13 @@ import { PersonAccount, formatName } from '@hcengineering/contact' import { Avatar, personByIdStore } from '@hcengineering/contact-resources' import { getCurrentAccount } from '@hcengineering/core' - import { getClient } from '@hcengineering/presentation' + import { getClient, playSound, stopSound } from '@hcengineering/presentation' import { Button, Label } from '@hcengineering/ui' import { JoinRequest, RequestStatus } from '@hcengineering/love' import love from '../plugin' import { myInfo, myOffice } from '../stores' import { connectRoom, isConnected } from '../utils' + import { onDestroy, onMount } from 'svelte' export let request: JoinRequest @@ -42,6 +43,12 @@ async function decline (): Promise { await client.update(request, { status: RequestStatus.Rejected }) } + onMount(() => { + playSound(love.sound.Knock, love.class.JoinRequest, true) + }) + onDestroy(() => { + stopSound(love.sound.Knock) + })
diff --git a/plugins/love/src/index.ts b/plugins/love/src/index.ts index 8867fd485a..960018c38c 100644 --- a/plugins/love/src/index.ts +++ b/plugins/love/src/index.ts @@ -140,6 +140,9 @@ const love = plugin(loveId, { ExitFullScreen: '' as Asset, Invite: '' as Asset }, + sound: { + Knock: '' as Asset + }, metadata: { WebSocketURL: '' as Metadata, ServiceEnpdoint: '' as Metadata diff --git a/plugins/notification-assets/lang/en.json b/plugins/notification-assets/lang/en.json index c5e29b0249..c8a8911d64 100644 --- a/plugins/notification-assets/lang/en.json +++ b/plugins/notification-assets/lang/en.json @@ -48,6 +48,7 @@ "Push": "Push", "Unreads": "Unreads", "EnablePush": "Enable push notifications", - "NotificationBlockedInBrowser": "Notifications are blocked in your browser. Please enable notifications in your browser settings" + "NotificationBlockedInBrowser": "Notifications are blocked in your browser. Please enable notifications in your browser settings", + "Sound": "Sound" } } diff --git a/plugins/notification-assets/lang/es.json b/plugins/notification-assets/lang/es.json index 8237c31222..57d99e3271 100644 --- a/plugins/notification-assets/lang/es.json +++ b/plugins/notification-assets/lang/es.json @@ -47,6 +47,7 @@ "UnstarDocument": "Desmarcar documento", "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." + "NotificationBlockedInBrowser": "Las notificaciones están bloqueadas en tu navegador. Por favor, habilita las notificaciones en la configuración de tu navegador.", + "Sound": "Sonido" } } \ No newline at end of file diff --git a/plugins/notification-assets/lang/fr.json b/plugins/notification-assets/lang/fr.json index 49d2b270ff..107d933f7a 100644 --- a/plugins/notification-assets/lang/fr.json +++ b/plugins/notification-assets/lang/fr.json @@ -48,6 +48,7 @@ "Push": "Push", "Unreads": "Non lus", "EnablePush": "Activer les notifications push", - "NotificationBlockedInBrowser": "Les notifications sont bloquées dans votre navigateur. Veuillez activer les notifications dans les paramètres de votre navigateur" + "NotificationBlockedInBrowser": "Les notifications sont bloquées dans votre navigateur. Veuillez activer les notifications dans les paramètres de votre navigateur", + "Sound": "Son" } } \ No newline at end of file diff --git a/plugins/notification-assets/lang/pt.json b/plugins/notification-assets/lang/pt.json index adbe17c023..b1cad6dfc9 100644 --- a/plugins/notification-assets/lang/pt.json +++ b/plugins/notification-assets/lang/pt.json @@ -47,6 +47,7 @@ "UnstarDocument": "Desmarcar documento", "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." + "NotificationBlockedInBrowser": "Notificações bloqueadas no navegador. Por favor habilite las notificaciones en la configuración de su navegador.", + "Sound": "Som" } } \ No newline at end of file diff --git a/plugins/notification-assets/lang/ru.json b/plugins/notification-assets/lang/ru.json index cc3165c383..c4e6aec3c8 100644 --- a/plugins/notification-assets/lang/ru.json +++ b/plugins/notification-assets/lang/ru.json @@ -48,6 +48,7 @@ "Push": "Push", "Unreads": "Непрочитанные", "EnablePush": "Включить Push-уведомления", - "NotificationBlockedInBrowser": "Уведомления заблокированы в вашем браузере. Пожалуйста, включите уведомления в настройках браузера" + "NotificationBlockedInBrowser": "Уведомления заблокированы в вашем браузере. Пожалуйста, включите уведомления в настройках браузера", + "Sound": "Звук" } } diff --git a/plugins/notification-assets/lang/zh.json b/plugins/notification-assets/lang/zh.json index 176fb9ac9c..f8aa9069e1 100644 --- a/plugins/notification-assets/lang/zh.json +++ b/plugins/notification-assets/lang/zh.json @@ -48,6 +48,7 @@ "Push": "推送", "Unreads": "未读", "EnablePush": "启用推送通知", - "NotificationBlockedInBrowser": "通知在您的浏览器中被阻止。请在浏览器设置中启用通知" + "NotificationBlockedInBrowser": "通知在您的浏览器中被阻止。请在浏览器设置中启用通知", + "Sound": "声音" } } diff --git a/plugins/notification/src/index.ts b/plugins/notification/src/index.ts index 93d2d2f24c..ac2d0481ae 100644 --- a/plugins/notification/src/index.ts +++ b/plugins/notification/src/index.ts @@ -352,7 +352,8 @@ const notification = plugin(notificationId, { providers: { PlatformNotification: '' as Ref, BrowserNotification: '' as Ref, - EmailNotification: '' as Ref + EmailNotification: '' as Ref, + SoundNotification: '' as Ref }, integrationType: { MobileApp: '' as Ref @@ -400,7 +401,8 @@ const notification = plugin(notificationId, { ArchiveAllConfirmationMessage: '' as IntlString, YouAddedCollaborators: '' as IntlString, YouRemovedCollaborators: '' as IntlString, - Push: '' as IntlString + Push: '' as IntlString, + Sound: '' as IntlString }, function: { Notify: '' as Resource, diff --git a/tests/sanity/tests/model/profile/notifications-page.ts b/tests/sanity/tests/model/profile/notifications-page.ts index 954ab0f73b..eb0b7cc7c6 100644 --- a/tests/sanity/tests/model/profile/notifications-page.ts +++ b/tests/sanity/tests/model/profile/notifications-page.ts @@ -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('div:nth-child(6) > .flex-between > .toggle > .toggle-switch') + chatMessageToggle = (): Locator => this.page.locator('div:nth-child(7) > .flex-between > .toggle > .toggle-switch') constructor (page: Page) { this.page = page