diff --git a/apps/meteor/app/custom-sounds/client/index.ts b/apps/meteor/app/custom-sounds/client/index.ts deleted file mode 100644 index 95992988ccfb1..0000000000000 --- a/apps/meteor/app/custom-sounds/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CustomSounds } from './lib/CustomSounds'; diff --git a/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts b/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts deleted file mode 100644 index f925caf7f8098..0000000000000 --- a/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts +++ /dev/null @@ -1,147 +0,0 @@ -import type { ICustomSound } from '@rocket.chat/core-typings'; -import { ReactiveVar } from 'meteor/reactive-var'; - -import { getURL } from '../../../utils/client'; -import { sdk } from '../../../utils/client/lib/SDKClient'; - -const getCustomSoundId = (soundId: ICustomSound['_id']) => `custom-sound-${soundId}`; -const getAssetUrl = (asset: string, params?: Record) => getURL(asset, params, undefined, true); - -const defaultSounds = [ - { _id: 'chime', name: 'Chime', extension: 'mp3', src: getAssetUrl('sounds/chime.mp3') }, - { _id: 'door', name: 'Door', extension: 'mp3', src: getAssetUrl('sounds/door.mp3') }, - { _id: 'beep', name: 'Beep', extension: 'mp3', src: getAssetUrl('sounds/beep.mp3') }, - { _id: 'chelle', name: 'Chelle', extension: 'mp3', src: getAssetUrl('sounds/chelle.mp3') }, - { _id: 'ding', name: 'Ding', extension: 'mp3', src: getAssetUrl('sounds/ding.mp3') }, - { _id: 'droplet', name: 'Droplet', extension: 'mp3', src: getAssetUrl('sounds/droplet.mp3') }, - { _id: 'highbell', name: 'Highbell', extension: 'mp3', src: getAssetUrl('sounds/highbell.mp3') }, - { _id: 'seasons', name: 'Seasons', extension: 'mp3', src: getAssetUrl('sounds/seasons.mp3') }, - { _id: 'telephone', name: 'Telephone', extension: 'mp3', src: getAssetUrl('sounds/telephone.mp3') }, - { _id: 'outbound-call-ringing', name: 'Outbound Call Ringing', extension: 'mp3', src: getAssetUrl('sounds/outbound-call-ringing.mp3') }, - { _id: 'call-ended', name: 'Call Ended', extension: 'mp3', src: getAssetUrl('sounds/call-ended.mp3') }, - { _id: 'dialtone', name: 'Dialtone', extension: 'mp3', src: getAssetUrl('sounds/dialtone.mp3') }, - { _id: 'ringtone', name: 'Ringtone', extension: 'mp3', src: getAssetUrl('sounds/ringtone.mp3') }, -]; - -class CustomSoundsClass { - list: ReactiveVar>; - - initialFetchDone: boolean; - - constructor() { - this.list = new ReactiveVar({}); - this.initialFetchDone = false; - defaultSounds.forEach((sound) => this.add(sound)); - } - - add(sound: ICustomSound) { - if (!sound.src) { - sound.src = this.getURL(sound); - } - - const source = document.createElement('source'); - source.src = sound.src; - - const audio = document.createElement('audio'); - audio.id = getCustomSoundId(sound._id); - audio.preload = 'none'; - audio.appendChild(source); - - document.body.appendChild(audio); - - const list = this.list.get(); - list[sound._id] = sound; - this.list.set(list); - } - - remove(sound: ICustomSound) { - const list = this.list.get(); - delete list[sound._id]; - this.list.set(list); - const audio = document.querySelector(`#${getCustomSoundId(sound._id)}`); - audio?.remove(); - } - - getSound(soundId: ICustomSound['_id']) { - const list = this.list.get(); - return list[soundId]; - } - - update(sound: ICustomSound) { - const audio = document.querySelector(`#${getCustomSoundId(sound._id)}`); - if (audio) { - const list = this.list.get(); - if (!sound.src) { - sound.src = this.getURL(sound); - } - list[sound._id] = sound; - this.list.set(list); - const sourceEl = audio.querySelector('source'); - if (sourceEl) { - sourceEl.src = sound.src; - } - audio.load(); - } else { - this.add(sound); - } - } - - getURL(sound: ICustomSound) { - return getAssetUrl(`/custom-sounds/${sound._id}.${sound.extension}`, { _dc: sound.random || 0 }); - } - - getList() { - const list = Object.values(this.list.get()); - return list.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? '')); - } - - play = async (soundId: ICustomSound['_id'], { volume = 1, loop = false } = {}) => { - const audio = document.querySelector(`#${getCustomSoundId(soundId)}`); - if (!audio?.play) { - return; - } - - audio.volume = volume; - audio.loop = loop; - await audio.play(); - - return audio; - }; - - pause = (soundId: ICustomSound['_id']) => { - const audio = document.querySelector(`#${getCustomSoundId(soundId)}`); - if (!audio?.pause) { - return; - } - - audio.pause(); - }; - - stop = (soundId: ICustomSound['_id']) => { - const audio = document.querySelector(`#${getCustomSoundId(soundId)}`); - if (!audio?.load) { - return; - } - - audio?.load(); - }; - - isPlaying = (soundId: ICustomSound['_id']) => { - const audio = document.querySelector(`#${getCustomSoundId(soundId)}`); - - return audio && audio.duration > 0 && !audio.paused; - }; - - fetchCustomSoundList = async () => { - if (this.initialFetchDone) { - return; - } - const result = await sdk.call('listCustomSounds'); - for (const sound of result) { - this.add(sound); - } - this.initialFetchDone = true; - }; -} - -export const CustomSounds = new CustomSoundsClass(); diff --git a/apps/meteor/app/utils/client/getUserNotificationsSoundVolume.tsx b/apps/meteor/app/utils/client/getUserNotificationsSoundVolume.tsx deleted file mode 100644 index ea89cd51d4e3b..0000000000000 --- a/apps/meteor/app/utils/client/getUserNotificationsSoundVolume.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; - -import { getUserPreference } from './lib/getUserPreference'; - -export const getUserNotificationsSoundVolume = (userId: IUser['_id'] | null | undefined) => { - const masterVolume = getUserPreference(userId, 'masterVolume', 100); - const notificationsSoundVolume = getUserPreference(userId, 'notificationsSoundVolume', 100); - - return (notificationsSoundVolume * masterVolume) / 100; -}; diff --git a/apps/meteor/client/hooks/notification/useNewMessageNotification.ts b/apps/meteor/client/hooks/notification/useNewMessageNotification.ts index 389fa010ffa32..36b94568a27a5 100644 --- a/apps/meteor/client/hooks/notification/useNewMessageNotification.ts +++ b/apps/meteor/client/hooks/notification/useNewMessageNotification.ts @@ -1,29 +1,24 @@ import type { AtLeast, ISubscription } from '@rocket.chat/core-typings'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useUserPreference } from '@rocket.chat/ui-contexts'; - -import { CustomSounds } from '../../../app/custom-sounds/client/lib/CustomSounds'; -import { useUserSoundPreferences } from '../useUserSoundPreferences'; +import { useCustomSound } from '@rocket.chat/ui-contexts'; export const useNewMessageNotification = () => { - const newMessageNotification = useUserPreference('newMessageNotification'); - const { notificationsSoundVolume } = useUserSoundPreferences(); + const { notificationSounds } = useCustomSound(); const notifyNewMessage = useEffectEvent((sub: AtLeast) => { if (!sub || sub.audioNotificationValue === 'none') { return; } - if (sub.audioNotificationValue && sub.audioNotificationValue !== '0') { - void CustomSounds.play(sub.audioNotificationValue, { - volume: Number((notificationsSoundVolume / 100).toPrecision(2)), - }); - } + // TODO: Fix this - Room Notifications Preferences > sound > desktop is not working. + // plays the user notificationSound preference - if (newMessageNotification && newMessageNotification !== 'none') { - void CustomSounds.play(newMessageNotification, { - volume: Number((notificationsSoundVolume / 100).toPrecision(2)), - }); - } + // if (sub.audioNotificationValue && sub.audioNotificationValue !== '0') { + // void CustomSounds.play(sub.audioNotificationValue, { + // volume: Number((notificationsSoundVolume / 100).toPrecision(2)), + // }); + // } + + notificationSounds.playNewMessage(); }); return notifyNewMessage; }; diff --git a/apps/meteor/client/hooks/notification/useNewRoomNotification.ts b/apps/meteor/client/hooks/notification/useNewRoomNotification.ts deleted file mode 100644 index ad2807404d070..0000000000000 --- a/apps/meteor/client/hooks/notification/useNewRoomNotification.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useUserPreference } from '@rocket.chat/ui-contexts'; - -import { CustomSounds } from '../../../app/custom-sounds/client/lib/CustomSounds'; -import { useUserSoundPreferences } from '../useUserSoundPreferences'; - -export const useNewRoomNotification = () => { - const newRoomNotification = useUserPreference('newRoomNotification'); - const { notificationsSoundVolume } = useUserSoundPreferences(); - - const notifyNewRoom = useEffectEvent(() => { - if (!newRoomNotification) { - return; - } - - void CustomSounds.play(newRoomNotification, { - volume: Number((notificationsSoundVolume / 100).toPrecision(2)), - }); - }); - - return notifyNewRoom; -}; diff --git a/apps/meteor/client/hooks/notification/useNotifyUser.ts b/apps/meteor/client/hooks/notification/useNotifyUser.ts index 7f2cfb918ab7e..d9de0d3115e31 100644 --- a/apps/meteor/client/hooks/notification/useNotifyUser.ts +++ b/apps/meteor/client/hooks/notification/useNotifyUser.ts @@ -1,12 +1,11 @@ import type { AtLeast, INotificationDesktop, ISubscription } from '@rocket.chat/core-typings'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useRouter, useStream, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useCustomSound, useRouter, useStream, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; import { useEmbeddedLayout } from '../useEmbeddedLayout'; import { useDesktopNotification } from './useDesktopNotification'; import { useNewMessageNotification } from './useNewMessageNotification'; -import { useNewRoomNotification } from './useNewRoomNotification'; import { CachedChatSubscription } from '../../../app/models/client'; import { RoomManager } from '../../lib/RoomManager'; import { fireGlobalEvent } from '../../lib/utils/fireGlobalEvent'; @@ -17,7 +16,7 @@ export const useNotifyUser = () => { const isLayoutEmbedded = useEmbeddedLayout(); const notifyUserStream = useStream('notify-user'); const muteFocusedConversations = useUserPreference('muteFocusedConversations'); - const newRoomNotification = useNewRoomNotification(); + const { notificationSounds } = useCustomSound(); const newMessageNotification = useNewMessageNotification(); const showDesktopNotification = useDesktopNotification(); @@ -27,7 +26,7 @@ export const useNotifyUser = () => { } if ((!router.getRouteParameters().name || router.getRouteParameters().name !== sub.name) && !sub.ls && sub.alert === true) { - newRoomNotification(); + notificationSounds.playNewRoom(); } }); @@ -83,6 +82,7 @@ export const useNotifyUser = () => { unsubNotification(); unsubSubs(); handle.stop(); + notificationSounds.stopNewRoom(); }; - }, [isLayoutEmbedded, notifyNewMessageAudioAndDesktop, notifyNewRoom, notifyUserStream, router, user?._id]); + }, [isLayoutEmbedded, notificationSounds, notifyNewMessageAudioAndDesktop, notifyNewRoom, notifyUserStream, router, user?._id]); }; diff --git a/apps/meteor/client/hooks/useOmnichannelContinuousSoundNotification.ts b/apps/meteor/client/hooks/useOmnichannelContinuousSoundNotification.ts index 639b4cb7af22e..d565aa74902d8 100644 --- a/apps/meteor/client/hooks/useOmnichannelContinuousSoundNotification.ts +++ b/apps/meteor/client/hooks/useOmnichannelContinuousSoundNotification.ts @@ -1,54 +1,28 @@ -import type { ICustomSound } from '@rocket.chat/core-typings'; -import { useSetting, useUserPreference, useUserSubscriptions } from '@rocket.chat/ui-contexts'; +import { useCustomSound, useSetting, useUserSubscriptions } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; -import { useUserSoundPreferences } from './useUserSoundPreferences'; -import { CustomSounds } from '../../app/custom-sounds/client/lib/CustomSounds'; - const query = { t: 'l', ls: { $exists: false }, open: true }; export const useOmnichannelContinuousSoundNotification = (queue: T[]) => { const userSubscriptions = useUserSubscriptions(query); + const { notificationSounds } = useCustomSound(); const playNewRoomSoundContinuously = useSetting('Livechat_continuous_sound_notification_new_livechat_room'); - const newRoomNotification = useUserPreference('newRoomNotification'); - const { notificationsSoundVolume } = useUserSoundPreferences(); - - const continuousCustomSoundId = newRoomNotification && `${newRoomNotification}-continuous`; - const hasUnreadRoom = userSubscriptions.length > 0 || queue.length > 0; useEffect(() => { - let audio: ICustomSound; - if (playNewRoomSoundContinuously && continuousCustomSoundId) { - audio = { ...CustomSounds.getSound(newRoomNotification), _id: continuousCustomSoundId }; - CustomSounds.add(audio); - } - - return () => { - if (audio) { - CustomSounds.remove(audio); - } - }; - }, [continuousCustomSoundId, newRoomNotification, playNewRoomSoundContinuously]); - - useEffect(() => { - if (!continuousCustomSoundId) { - return; - } if (!playNewRoomSoundContinuously) { - CustomSounds.pause(continuousCustomSoundId); return; } if (!hasUnreadRoom) { - CustomSounds.pause(continuousCustomSoundId); return; } - CustomSounds.play(continuousCustomSoundId, { - volume: notificationsSoundVolume / 100, - loop: true, - }); - }, [continuousCustomSoundId, playNewRoomSoundContinuously, userSubscriptions, notificationsSoundVolume, hasUnreadRoom]); + notificationSounds.playNewMessageLoop(); + + return () => { + notificationSounds.stopNewRoom(); + }; + }, [playNewRoomSoundContinuously, userSubscriptions, hasUnreadRoom, notificationSounds]); }; diff --git a/apps/meteor/client/importPackages.ts b/apps/meteor/client/importPackages.ts index d47cc47eae4d7..d82f6460d024f 100644 --- a/apps/meteor/client/importPackages.ts +++ b/apps/meteor/client/importPackages.ts @@ -1,7 +1,6 @@ import '../app/apple/client'; import '../app/authorization/client'; import '../app/autotranslate/client'; -import '../app/custom-sounds/client'; import '../app/emoji/client'; import '../app/emoji-emojione/client'; import '../app/emoji-custom/client'; diff --git a/apps/meteor/client/providers/CallProvider/CallProvider.tsx b/apps/meteor/client/providers/CallProvider/CallProvider.tsx index fd86f6376ee8d..7448b4160aa8c 100644 --- a/apps/meteor/client/providers/CallProvider/CallProvider.tsx +++ b/apps/meteor/client/providers/CallProvider/CallProvider.tsx @@ -22,13 +22,13 @@ import { useSetInputMediaDevice, useSetModal, useTranslation, + useCustomSound, } from '@rocket.chat/ui-contexts'; import type { ReactNode } from 'react'; import { useMemo, useRef, useCallback, useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import type { OutgoingByeRequest } from 'sip.js/lib/core'; -import { useVoipSounds } from './hooks/useVoipSounds'; import type { CallContextValue } from '../../contexts/CallContext'; import { CallContext, useIsVoipEnterprise } from '../../contexts/CallContext'; import { useDialModal } from '../../hooks/useDialModal'; @@ -73,7 +73,7 @@ export const CallProvider = ({ children }: CallProviderProps) => { const { openDialModal } = useDialModal(); - const voipSounds = useVoipSounds(); + const { voipSounds } = useCustomSound(); const closeRoom = useCallback( async ( @@ -336,7 +336,9 @@ export const CallProvider = ({ children }: CallProviderProps) => { if (!callDetails.callInfo) { return; } + voipSounds.stopAll(); + if (callDetails.userState !== UserState.UAC) { return; } @@ -377,15 +379,15 @@ export const CallProvider = ({ children }: CallProviderProps) => { }; const onRinging = (): void => { - voipSounds.play('outbound-call-ringing'); + voipSounds.playDialer(); }; const onIncomingCallRinging = (): void => { - voipSounds.play('telephone'); + voipSounds.playRinger(); }; const onCallTerminated = (): void => { - voipSounds.play('call-ended', false); + voipSounds.playCallEnded(); voipSounds.stopAll(); }; diff --git a/apps/meteor/client/providers/CallProvider/hooks/useVoipSounds.ts b/apps/meteor/client/providers/CallProvider/hooks/useVoipSounds.ts deleted file mode 100644 index 7eea3b867f507..0000000000000 --- a/apps/meteor/client/providers/CallProvider/hooks/useVoipSounds.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useCustomSound } from '@rocket.chat/ui-contexts'; -import { useMemo } from 'react'; - -import { useUserSoundPreferences } from '../../../hooks/useUserSoundPreferences'; - -type VoipSound = 'telephone' | 'outbound-call-ringing' | 'call-ended'; - -export const useVoipSounds = () => { - const { play, pause } = useCustomSound(); - const { voipRingerVolume } = useUserSoundPreferences(); - - return useMemo( - () => ({ - play: (soundId: VoipSound, loop = true) => { - play(soundId, { - volume: Number((voipRingerVolume / 100).toPrecision(2)), - loop, - }); - }, - stop: (soundId: VoipSound) => pause(soundId), - stopAll: () => { - pause('telephone'); - pause('outbound-call-ringing'); - }, - }), - [play, pause, voipRingerVolume], - ); -}; diff --git a/apps/meteor/client/providers/CustomSoundProvider.tsx b/apps/meteor/client/providers/CustomSoundProvider.tsx deleted file mode 100644 index 454b16196422f..0000000000000 --- a/apps/meteor/client/providers/CustomSoundProvider.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { CustomSoundContext, useUserId, useStream } from '@rocket.chat/ui-contexts'; -import type { ReactNode } from 'react'; -import { useEffect } from 'react'; - -import { CustomSounds } from '../../app/custom-sounds/client/lib/CustomSounds'; - -type CustomSoundProviderProps = { - children?: ReactNode; -}; - -const CustomSoundProvider = ({ children }: CustomSoundProviderProps) => { - const userId = useUserId(); - useEffect(() => { - if (!userId) { - return; - } - void CustomSounds.fetchCustomSoundList(); - }, [userId]); - - const streamAll = useStream('notify-all'); - - useEffect(() => { - if (!userId) { - return; - } - - return streamAll('public-info', ([key, data]) => { - switch (key) { - case 'updateCustomSound': - CustomSounds.update(data[0].soundData); - break; - case 'deleteCustomSound': - CustomSounds.remove(data[0].soundData); - break; - } - }); - }, [userId, streamAll]); - return ; -}; - -export default CustomSoundProvider; diff --git a/apps/meteor/client/providers/CustomSoundProvider/CustomSoundProvider.tsx b/apps/meteor/client/providers/CustomSoundProvider/CustomSoundProvider.tsx new file mode 100644 index 0000000000000..1f13a15600859 --- /dev/null +++ b/apps/meteor/client/providers/CustomSoundProvider/CustomSoundProvider.tsx @@ -0,0 +1,123 @@ +import type { ICustomSound } from '@rocket.chat/core-typings'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { CustomSoundContext, useStream, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useEffect, useRef, type ReactNode } from 'react'; + +import { defaultSounds, formatVolume, getCustomSoundURL } from './lib/helpers'; +import { sdk } from '../../../app/utils/client/lib/SDKClient'; +import { useUserSoundPreferences } from '../../hooks/useUserSoundPreferences'; + +type CustomSoundProviderProps = { + children?: ReactNode; +}; + +const CustomSoundProvider = ({ children }: CustomSoundProviderProps) => { + const audioRefs = useRef([]); + + const queryClient = useQueryClient(); + const streamAll = useStream('notify-all'); + + const newRoomNotification = useUserPreference('newRoomNotification') || 'door'; + const newMessageNotification = useUserPreference('newMessageNotification') || 'chime'; + const { notificationsSoundVolume, voipRingerVolume } = useUserSoundPreferences(); + + const { data: list } = useQuery({ + queryFn: async () => { + const customSoundsList = await sdk.call('listCustomSounds'); + if (!customSoundsList.length) { + return defaultSounds; + } + return [...customSoundsList.map((sound) => ({ ...sound, src: getCustomSoundURL(sound) })), ...defaultSounds]; + }, + queryKey: ['listCustomSounds'], + initialData: defaultSounds, + }); + + const play = useEffectEvent((soundId: ICustomSound['_id'], { volume = 1, loop = false } = {}) => { + stop(soundId); + + const item = list?.find(({ _id }) => _id === soundId); + if (!item?.src) { + console.error('Unable to play sound', soundId); + return; + } + + const audio = new Audio(item.src); + audio.volume = volume; + audio.loop = loop; + audio.id = soundId; + audio.play(); + + audioRefs.current = [...audioRefs.current, audio]; + + return () => { + stop(soundId); + }; + }); + + const pause = useEffectEvent((soundId: ICustomSound['_id']) => { + const current = audioRefs.current?.find(({ id }) => id === soundId); + if (current) { + current.pause(); + audioRefs.current = audioRefs.current.filter(({ id }) => id !== soundId); + } + }); + + const stop = useEffectEvent((soundId: ICustomSound['_id']) => { + const current = audioRefs.current?.find(({ id }) => id === soundId); + if (current) { + current.load(); + audioRefs.current = audioRefs.current.filter(({ id }) => id !== soundId); + } + }); + + const callSounds = { + playRinger: () => play('ringtone', { loop: true, volume: formatVolume(voipRingerVolume) }), + playDialer: () => play('dialtone', { loop: true, volume: formatVolume(voipRingerVolume) }), + stopRinger: () => stop('ringtone'), + stopDialer: () => stop('dialtone'), + }; + + const voipSounds = { + playRinger: () => play('telephone', { loop: true, volume: formatVolume(voipRingerVolume) }), + playDialer: () => play('outbound-call-ringing', { loop: true, volume: formatVolume(voipRingerVolume) }), + playCallEnded: () => play('call-ended', { loop: false, volume: formatVolume(voipRingerVolume) }), + stopRinger: () => stop('telephone'), + stopDialer: () => stop('outbound-call-ringing'), + stopCallEnded: () => stop('call-ended'), + stopAll: () => { + stop('telephone'); + stop('outbound-call-ringing'); + stop('call-ended'); + }, + }; + + const notificationSounds = { + playNewRoom: () => play(newRoomNotification, { loop: false, volume: formatVolume(notificationsSoundVolume) }), + playNewMessage: () => play(newMessageNotification, { loop: false, volume: formatVolume(notificationsSoundVolume) }), + playNewMessageLoop: () => play(newMessageNotification, { loop: true, volume: formatVolume(notificationsSoundVolume) }), + stopNewRoom: () => stop(newRoomNotification), + stopNewMessage: () => stop(newMessageNotification), + }; + + useEffect(() => { + return streamAll('public-info', ([key]) => { + switch (key) { + case 'updateCustomSound': + queryClient.invalidateQueries({ queryKey: ['listCustomSounds'] }); + break; + case 'deleteCustomSound': + queryClient.invalidateQueries({ queryKey: ['listCustomSounds'] }); + + break; + } + }); + }, [queryClient, streamAll]); + + return ( + + ); +}; + +export default CustomSoundProvider; diff --git a/apps/meteor/client/providers/CustomSoundProvider/index.ts b/apps/meteor/client/providers/CustomSoundProvider/index.ts new file mode 100644 index 0000000000000..a2e563004d267 --- /dev/null +++ b/apps/meteor/client/providers/CustomSoundProvider/index.ts @@ -0,0 +1 @@ +export { default } from './CustomSoundProvider'; diff --git a/apps/meteor/client/providers/CustomSoundProvider/lib/helpers.ts b/apps/meteor/client/providers/CustomSoundProvider/lib/helpers.ts new file mode 100644 index 0000000000000..48e955e5dc2ce --- /dev/null +++ b/apps/meteor/client/providers/CustomSoundProvider/lib/helpers.ts @@ -0,0 +1,29 @@ +import type { ICustomSound } from '@rocket.chat/core-typings'; + +import { getURL } from '../../../../app/utils/client'; + +export const getAssetUrl = (asset: string, params?: Record) => getURL(asset, params, undefined, true); + +export const getCustomSoundURL = (sound: ICustomSound) => { + return getAssetUrl(`/custom-sounds/${sound._id}.${sound.extension}`, { _dc: sound.random || 0 }); +}; + +export const defaultSounds: ICustomSound[] = [ + { _id: 'chime', name: 'Chime', extension: 'mp3', src: getAssetUrl('sounds/chime.mp3') }, + { _id: 'door', name: 'Door', extension: 'mp3', src: getAssetUrl('sounds/door.mp3') }, + { _id: 'beep', name: 'Beep', extension: 'mp3', src: getAssetUrl('sounds/beep.mp3') }, + { _id: 'chelle', name: 'Chelle', extension: 'mp3', src: getAssetUrl('sounds/chelle.mp3') }, + { _id: 'ding', name: 'Ding', extension: 'mp3', src: getAssetUrl('sounds/ding.mp3') }, + { _id: 'droplet', name: 'Droplet', extension: 'mp3', src: getAssetUrl('sounds/droplet.mp3') }, + { _id: 'highbell', name: 'Highbell', extension: 'mp3', src: getAssetUrl('sounds/highbell.mp3') }, + { _id: 'seasons', name: 'Seasons', extension: 'mp3', src: getAssetUrl('sounds/seasons.mp3') }, + { _id: 'telephone', name: 'Telephone', extension: 'mp3', src: getAssetUrl('sounds/telephone.mp3') }, + { _id: 'outbound-call-ringing', name: 'Outbound Call Ringing', extension: 'mp3', src: getAssetUrl('sounds/outbound-call-ringing.mp3') }, + { _id: 'call-ended', name: 'Call Ended', extension: 'mp3', src: getAssetUrl('sounds/call-ended.mp3') }, + { _id: 'dialtone', name: 'Dialtone', extension: 'mp3', src: getAssetUrl('sounds/dialtone.mp3') }, + { _id: 'ringtone', name: 'Ringtone', extension: 'mp3', src: getAssetUrl('sounds/ringtone.mp3') }, +]; + +export const formatVolume = (volume: number) => { + return Number((volume / 100).toPrecision(2)); +}; diff --git a/apps/meteor/client/providers/OmnichannelProvider.tsx b/apps/meteor/client/providers/OmnichannelProvider.tsx index cb39b9a4abafb..ee0f9e6fb0e22 100644 --- a/apps/meteor/client/providers/OmnichannelProvider.tsx +++ b/apps/meteor/client/providers/OmnichannelProvider.tsx @@ -6,7 +6,7 @@ import { } from '@rocket.chat/core-typings'; import { useSafely } from '@rocket.chat/fuselage-hooks'; import { createComparatorFromSort } from '@rocket.chat/mongo-adapter'; -import { useUser, useSetting, usePermission, useMethod, useEndpoint, useStream } from '@rocket.chat/ui-contexts'; +import { useUser, useSetting, usePermission, useMethod, useEndpoint, useStream, useCustomSound } from '@rocket.chat/ui-contexts'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import type { ReactNode } from 'react'; import { useState, useEffect, useMemo, memo, useRef } from 'react'; @@ -17,7 +17,6 @@ import { getOmniChatSortQuery } from '../../app/livechat/lib/inquiries'; import { ClientLogger } from '../../lib/ClientLogger'; import type { OmnichannelContextValue } from '../contexts/OmnichannelContext'; import { OmnichannelContext } from '../contexts/OmnichannelContext'; -import { useNewRoomNotification } from '../hooks/notification/useNewRoomNotification'; import { useHasLicenseModule } from '../hooks/useHasLicenseModule'; import { useLivechatInquiryStore } from '../hooks/useLivechatInquiryStore'; import { useOmnichannelContinuousSoundNotification } from '../hooks/useOmnichannelContinuousSoundNotification'; @@ -76,7 +75,7 @@ const OmnichannelProvider = ({ children }: OmnichannelProviderProps) => { const isPrioritiesEnabled = isEnterprise && accessible; const enabled = accessible && !!user && !!routeConfig; - const notifyNewRoom = useNewRoomNotification(); + const { notificationSounds } = useCustomSound(); const { data: { priorities = [] } = {}, @@ -157,10 +156,14 @@ const OmnichannelProvider = ({ children }: OmnichannelProviderProps) => { useEffect(() => { if (lastQueueSize.current < (queue?.length ?? 0)) { - notifyNewRoom(); + notificationSounds.playNewRoom(); } lastQueueSize.current = queue?.length ?? 0; - }, [notifyNewRoom, queue?.length]); + + return () => { + notificationSounds.stopNewRoom(); + }; + }, [notificationSounds, queue?.length]); useOmnichannelContinuousSoundNotification(queue ?? []); diff --git a/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx index 153d03dd078dc..3599e3d381d9c 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx @@ -8,7 +8,7 @@ const PreferencesSoundSection = () => { const t = useTranslation(); const customSound = useCustomSound(); - const soundsList: SelectOption[] = customSound?.getList()?.map((value) => [value._id, value.name]) || []; + const soundsList: SelectOption[] = customSound.list?.map((value) => [value._id, value.name]) || []; const { control, watch } = useFormContext(); const { newMessageNotification, notificationsSoundVolume = 100, masterVolume = 100, voipRingerVolume = 100 } = watch(); diff --git a/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferencesWithData.tsx b/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferencesWithData.tsx index 07d8732c5dd80..cbe465a60d7ec 100644 --- a/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferencesWithData.tsx +++ b/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferencesWithData.tsx @@ -20,7 +20,7 @@ const NotificationPreferencesWithData = (): ReactElement => { successMessage: t('Room_updated_successfully'), }); - const customSoundAsset: SelectOption[] | undefined = customSound?.getList()?.map((value) => [value._id, value.name]); + const customSoundAsset: SelectOption[] | undefined = customSound.list?.map((value) => [value._id, value.name]); const defaultOption: SelectOption[] = [ ['default', t('Default')], diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopups.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopups.tsx index d028a2620741e..7ba4d58cbc705 100644 --- a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopups.tsx +++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopups.tsx @@ -11,15 +11,13 @@ import { useEffect, useMemo } from 'react'; import { FocusScope } from 'react-aria'; import VideoConfPopup from './VideoConfPopup'; -import { useUserSoundPreferences } from '../../../../../hooks/useUserSoundPreferences'; import VideoConfPopupPortal from '../../../../../portals/VideoConfPopupPortal'; const VideoConfPopups = ({ children }: { children?: VideoConfPopupPayload }): ReactElement => { - const customSound = useCustomSound(); + const { callSounds } = useCustomSound(); const incomingCalls = useVideoConfIncomingCalls(); const isRinging = useVideoConfIsRinging(); const isCalling = useVideoConfIsCalling(); - const { voipRingerVolume } = useUserSoundPreferences(); const popups = useMemo( () => @@ -31,18 +29,18 @@ const VideoConfPopups = ({ children }: { children?: VideoConfPopupPayload }): Re useEffect(() => { if (isRinging) { - customSound.play('ringtone', { loop: true, volume: voipRingerVolume / 100 }); + callSounds.playRinger(); } if (isCalling) { - customSound.play('dialtone', { loop: true, volume: voipRingerVolume / 100 }); + callSounds.playDialer(); } return (): void => { - customSound.stop('ringtone'); - customSound.stop('dialtone'); + callSounds.stopRinger(); + callSounds.stopDialer(); }; - }, [customSound, isRinging, isCalling, voipRingerVolume]); + }, [isRinging, isCalling, callSounds]); return ( <> diff --git a/apps/meteor/tests/e2e/video-conference-ring.spec.ts b/apps/meteor/tests/e2e/video-conference-ring.spec.ts index d5072b00b5cca..d489e1a258ac0 100644 --- a/apps/meteor/tests/e2e/video-conference-ring.spec.ts +++ b/apps/meteor/tests/e2e/video-conference-ring.spec.ts @@ -10,13 +10,11 @@ test.use({ storageState: Users.user1.state }); test.describe('video conference ringing', () => { let poHomeChannel: HomeChannel; - let poAccountProfile: AccountProfile; test.skip(!IS_EE, 'Enterprise Only'); test.beforeEach(async ({ page }) => { poHomeChannel = new HomeChannel(page); - poAccountProfile = new AccountProfile(page); await page.goto('/home'); }); @@ -51,39 +49,4 @@ test.describe('video conference ringing', () => { await expect(poHomeChannel.content.getVideoConfPopupByName('Start a call with user2')).toBeVisible(); }); }); - - const changeCallRingerVolumeFromHome = async (poHomeChannel: HomeChannel, poAccountProfile: AccountProfile, volume: string) => { - await poHomeChannel.sidenav.userProfileMenu.click(); - await poHomeChannel.sidenav.accountPreferencesOption.click(); - - await poAccountProfile.preferencesSoundAccordionOption.click(); - await poAccountProfile.preferencesCallRingerVolumeSlider.fill(volume); - - await poAccountProfile.btnSaveChanges.click(); - await poAccountProfile.btnClose.click(); - }; - - test('should be ringing/dialing according to volume preference', async () => { - await changeCallRingerVolumeFromHome(poHomeChannel, poAccountProfile, '50'); - await changeCallRingerVolumeFromHome(auxContext.poHomeChannel, auxContext.poAccountProfile, '25'); - - await poHomeChannel.sidenav.openChat('user2'); - await auxContext.poHomeChannel.sidenav.openChat('user1'); - - await poHomeChannel.content.btnCall.click(); - await poHomeChannel.content.menuItemVideoCall.click(); - await poHomeChannel.content.btnStartVideoCall.click(); - - await expect(auxContext.poHomeChannel.content.getVideoConfPopupByName('Incoming call from user1')).toBeVisible(); - - const dialToneVolume = await poHomeChannel.audioVideoConfDialtone.evaluate((el: HTMLAudioElement) => el.volume); - const ringToneVolume = await auxContext.poHomeChannel.audioVideoConfRingtone.evaluate((el: HTMLAudioElement) => el.volume); - - expect(dialToneVolume).toBe(0.5); - expect(ringToneVolume).toBe(0.25); - - await auxContext.poHomeChannel.content.btnDeclineVideoCall.click(); - await changeCallRingerVolumeFromHome(poHomeChannel, poAccountProfile, '100'); - await changeCallRingerVolumeFromHome(auxContext.poHomeChannel, auxContext.poAccountProfile, '100'); - }); }); diff --git a/packages/ui-contexts/src/CustomSoundContext.ts b/packages/ui-contexts/src/CustomSoundContext.ts index 30a48b603d5d4..5c3838ba47c6a 100644 --- a/packages/ui-contexts/src/CustomSoundContext.ts +++ b/packages/ui-contexts/src/CustomSoundContext.ts @@ -2,17 +2,67 @@ import type { ICustomSound } from '@rocket.chat/core-typings'; import { createContext } from 'react'; export type CustomSoundContextValue = { - play: (sound: ICustomSound['_id'], options?: { volume?: number; loop?: boolean }) => Promise; + play: ( + soundId: string, + options?: + | { + volume?: number | undefined; + loop?: boolean | undefined; + } + | undefined, + ) => void; pause: (sound: ICustomSound['_id']) => void; stop: (sound: ICustomSound['_id']) => void; - getList: () => ICustomSound[] | undefined; - isPlaying: (sound: ICustomSound['_id']) => boolean | null; + callSounds: { + playRinger: () => void; + playDialer: () => void; + stopRinger: () => void; + stopDialer: () => void; + }; + voipSounds: { + playRinger: () => void; + playDialer: () => void; + playCallEnded: () => void; + stopRinger: () => void; + stopDialer: () => void; + stopCallEnded: () => void; + stopAll: () => void; + }; + notificationSounds: { + playNewRoom: () => void; + playNewMessage: () => void; + playNewMessageLoop: () => void; + stopNewRoom: () => void; + stopNewMessage: () => void; + }; + list: ICustomSound[]; }; export const CustomSoundContext = createContext({ play: () => new Promise(() => undefined), pause: () => undefined, stop: () => undefined, - getList: () => undefined, - isPlaying: () => false, + callSounds: { + playRinger: () => undefined, + playDialer: () => undefined, + stopRinger: () => undefined, + stopDialer: () => undefined, + }, + voipSounds: { + playRinger: () => undefined, + playDialer: () => undefined, + playCallEnded: () => undefined, + stopRinger: () => undefined, + stopDialer: () => undefined, + stopCallEnded: () => undefined, + stopAll: () => undefined, + }, + notificationSounds: { + playNewRoom: () => undefined, + playNewMessage: () => undefined, + playNewMessageLoop: () => undefined, + stopNewRoom: () => undefined, + stopNewMessage: () => undefined, + }, + list: [], }); diff --git a/packages/ui-voip/src/hooks/useVoipSounds.ts b/packages/ui-voip/src/hooks/useVoipSounds.ts deleted file mode 100644 index f08ff00e3da5a..0000000000000 --- a/packages/ui-voip/src/hooks/useVoipSounds.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useCustomSound, useUserPreference } from '@rocket.chat/ui-contexts'; -import { useMemo } from 'react'; - -type VoipSound = 'telephone' | 'outbound-call-ringing' | 'call-ended'; - -export const useVoipSounds = () => { - const { play, pause } = useCustomSound(); - const masterVolume = useUserPreference('masterVolume', 100) || 100; - const voipRingerVolume = useUserPreference('voipRingerVolume', 100) || 100; - const audioVolume = Math.floor((voipRingerVolume * masterVolume) / 100); - - return useMemo( - () => ({ - play: (soundId: VoipSound, loop = true) => { - play(soundId, { - volume: Number((audioVolume / 100).toPrecision(2)), - loop, - }); - }, - stop: (soundId: VoipSound) => pause(soundId), - stopAll: () => { - pause('telephone'); - pause('outbound-call-ringing'); - }, - }), - [play, pause, audioVolume], - ); -}; diff --git a/packages/ui-voip/src/providers/VoipProvider.tsx b/packages/ui-voip/src/providers/VoipProvider.tsx index d6d302fffd0e5..46a0c8b552f87 100644 --- a/packages/ui-voip/src/providers/VoipProvider.tsx +++ b/packages/ui-voip/src/providers/VoipProvider.tsx @@ -1,6 +1,7 @@ import { useEffectEvent, useLocalStorage } from '@rocket.chat/fuselage-hooks'; import type { Device } from '@rocket.chat/ui-contexts'; import { + useCustomSound, usePermission, useSetInputMediaDevice, useSetOutputMediaDevice, @@ -17,7 +18,6 @@ import VoipPopupPortal from '../components/VoipPopupPortal'; import type { VoipContextValue } from '../contexts/VoipContext'; import { VoipContext } from '../contexts/VoipContext'; import { useVoipClient } from '../hooks/useVoipClient'; -import { useVoipSounds } from '../hooks/useVoipSounds'; const VoipProvider = ({ children }: { children: ReactNode }) => { // Settings @@ -29,7 +29,7 @@ const VoipProvider = ({ children }: { children: ReactNode }) => { // Hooks const { t } = useTranslation(); - const voipSounds = useVoipSounds(); + const { voipSounds } = useCustomSound(); const { voipClient, error } = useVoipClient({ enabled: isVoipEnabled, autoRegister: isLocalRegistered, @@ -57,7 +57,8 @@ const VoipProvider = ({ children }: { children: ReactNode }) => { }; const onCallEstablished = async (): Promise => { - voipSounds.stopAll(); + voipSounds.stopDialer(); + voipSounds.stopRinger(); window.addEventListener('beforeunload', onBeforeUnload); }; @@ -68,16 +69,18 @@ const VoipProvider = ({ children }: { children: ReactNode }) => { }; const onOutgoingCallRinging = (): void => { - voipSounds.play('outbound-call-ringing'); + voipSounds.playDialer(); }; const onIncomingCallRinging = (): void => { - voipSounds.play('telephone'); + voipSounds.playRinger(); }; const onCallTerminated = (): void => { - voipSounds.play('call-ended', false); - voipSounds.stopAll(); + voipSounds.playCallEnded(); + voipSounds.stopCallEnded(); + voipSounds.stopDialer(); + voipSounds.stopRinger(); window.removeEventListener('beforeunload', onBeforeUnload); };