diff --git a/.changeset/mean-numbers-chew.md b/.changeset/mean-numbers-chew.md new file mode 100644 index 000000000000..b4e3d6acfe39 --- /dev/null +++ b/.changeset/mean-numbers-chew.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": patch +"@rocket.chat/rest-typings": patch +"@rocket.chat/ui-voip": patch +--- + +Enables control of video conference ringing and dialing sounds through the call ringer volume user preference, preventing video conf calls from always playing at maximum volume. diff --git a/apps/meteor/client/hooks/useUserSoundPreferences.ts b/apps/meteor/client/hooks/useUserSoundPreferences.ts index e59a55648f55..d9027f14f47a 100644 --- a/apps/meteor/client/hooks/useUserSoundPreferences.ts +++ b/apps/meteor/client/hooks/useUserSoundPreferences.ts @@ -1,17 +1,15 @@ import { useUserPreference } from '@rocket.chat/ui-contexts'; -const relativeVolume = (volume: number, masterVolume: number) => { - return (volume * masterVolume) / 100; -}; +const relativeVolume = (volume: number, masterVolume: number) => (volume * masterVolume) / 100; export const useUserSoundPreferences = () => { - const masterVolume = useUserPreference('masterVolume', 100) || 100; - const notificationsSoundVolume = useUserPreference('notificationsSoundVolume', 100) || 100; - const voipRingerVolume = useUserPreference('voipRingerVolume', 100) || 100; + const masterVolume = useUserPreference('masterVolume', 100) ?? 100; + const notificationsSoundVolume = useUserPreference('notificationsSoundVolume', 100) ?? 100; + const callRingerVolume = useUserPreference('callRingerVolume', 100) ?? 100; return { masterVolume, notificationsSoundVolume: relativeVolume(notificationsSoundVolume, masterVolume), - voipRingerVolume: relativeVolume(voipRingerVolume, masterVolume), + callRingerVolume: relativeVolume(callRingerVolume, masterVolume), }; }; diff --git a/apps/meteor/client/providers/CallProvider/hooks/useVoipSounds.ts b/apps/meteor/client/providers/CallProvider/hooks/useVoipSounds.ts index 7eea3b867f50..19016bd40ee0 100644 --- a/apps/meteor/client/providers/CallProvider/hooks/useVoipSounds.ts +++ b/apps/meteor/client/providers/CallProvider/hooks/useVoipSounds.ts @@ -7,13 +7,13 @@ type VoipSound = 'telephone' | 'outbound-call-ringing' | 'call-ended'; export const useVoipSounds = () => { const { play, pause } = useCustomSound(); - const { voipRingerVolume } = useUserSoundPreferences(); + const { callRingerVolume } = useUserSoundPreferences(); return useMemo( () => ({ play: (soundId: VoipSound, loop = true) => { play(soundId, { - volume: Number((voipRingerVolume / 100).toPrecision(2)), + volume: Number((callRingerVolume / 100).toPrecision(2)), loop, }); }, @@ -23,6 +23,6 @@ export const useVoipSounds = () => { pause('outbound-call-ringing'); }, }), - [play, pause, voipRingerVolume], + [play, pause, callRingerVolume], ); }; diff --git a/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx index 6f1d92f64200..d2939700412f 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx @@ -10,14 +10,14 @@ const PreferencesSoundSection = () => { const customSound = useCustomSound(); const soundsList: SelectOption[] = customSound?.getList()?.map((value) => [value._id, value.name]) || []; const { control, watch } = useFormContext(); - const { newMessageNotification, notificationsSoundVolume = 100, masterVolume = 100, voipRingerVolume = 100 } = watch(); + const { newMessageNotification, notificationsSoundVolume = 100, masterVolume = 100, callRingerVolume = 100 } = watch(); const newRoomNotificationId = useId(); const newMessageNotificationId = useId(); const muteFocusedConversationsId = useId(); const masterVolumeId = useId(); const notificationsSoundVolumeId = useId(); - const voipRingerVolumeId = useId(); + const callRingerVolumeId = useId(); return ( @@ -33,7 +33,7 @@ const PreferencesSoundSection = () => { control={control} render={({ field: { onChange, value } }) => ( { control={control} render={({ field: { onChange, value } }) => ( { - {t('Call_ringer_volume')} - + {t('Call_ringer_volume')} + {t('Call_ringer_volume_hint')} ( { - const soundVolume = (voipRingerVolume * masterVolume) / 100; + const soundVolume = (callRingerVolume * masterVolume) / 100; customSound.play('telephone', { volume: soundVolume / 100 }); onChange(value); }} diff --git a/apps/meteor/client/views/account/preferences/useAccountPreferencesValues.ts b/apps/meteor/client/views/account/preferences/useAccountPreferencesValues.ts index e6520b07f34d..838a3515e836 100644 --- a/apps/meteor/client/views/account/preferences/useAccountPreferencesValues.ts +++ b/apps/meteor/client/views/account/preferences/useAccountPreferencesValues.ts @@ -37,7 +37,7 @@ export type AccountPreferencesData = { sidebarGroupByType?: boolean; masterVolume?: number; notificationsSoundVolume?: number; - voipRingerVolume?: number; + callRingerVolume?: number; }; export const useAccountPreferencesValues = (): AccountPreferencesData => { @@ -75,7 +75,7 @@ export const useAccountPreferencesValues = (): AccountPreferencesData => { const masterVolume = useUserPreference('masterVolume', 100); const notificationsSoundVolume = useUserPreference('notificationsSoundVolume', 100); - const voipRingerVolume = useUserPreference('voipRingerVolume', 100); + const callRingerVolume = useUserPreference('callRingerVolume', 100); return { language, @@ -106,6 +106,6 @@ export const useAccountPreferencesValues = (): AccountPreferencesData => { muteFocusedConversations, masterVolume, notificationsSoundVolume, - voipRingerVolume, + callRingerVolume, }; }; diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/IncomingPopup.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/IncomingPopup.tsx index bbf197b7223d..2b7640444f7d 100644 --- a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/IncomingPopup.tsx +++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/IncomingPopup.tsx @@ -21,6 +21,7 @@ import { useTranslation } from 'react-i18next'; import VideoConfPopupRoomInfo from './VideoConfPopupRoomInfo'; import { AsyncStatePhase } from '../../../../../../hooks/useAsyncState'; import { useEndpointData } from '../../../../../../hooks/useEndpointData'; +import { useVideoConfRoomName } from '../../hooks/useVideoConfRoomName'; type IncomingPopupProps = { id: string; @@ -35,6 +36,7 @@ const IncomingPopup = ({ id, room, position, onClose, onMute, onConfirm }: Incom const { t } = useTranslation(); const { controllersConfig, handleToggleMic, handleToggleCam } = useVideoConfControllers(); const setPreferences = useVideoConfSetPreferences(); + const roomName = useVideoConfRoomName(room); const params = useMemo(() => ({ callId: id }), [id]); const { phase, value } = useEndpointData('/v1/video-conference.info', { params }); @@ -47,7 +49,7 @@ const IncomingPopup = ({ id, room, position, onClose, onMute, onConfirm }: Incom }); return ( - + {phase === AsyncStatePhase.LOADING && } 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 d985cc03a0f7..216df152e9e3 100644 --- a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopups.tsx +++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopups.tsx @@ -11,6 +11,7 @@ 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 => { @@ -18,6 +19,7 @@ const VideoConfPopups = ({ children }: { children?: VideoConfPopupPayload }): Re const incomingCalls = useVideoConfIncomingCalls(); const isRinging = useVideoConfIsRinging(); const isCalling = useVideoConfIsCalling(); + const { callRingerVolume } = useUserSoundPreferences(); const popups = useMemo( () => @@ -29,18 +31,18 @@ const VideoConfPopups = ({ children }: { children?: VideoConfPopupPayload }): Re useEffect(() => { if (isRinging) { - customSound.play('ringtone', { loop: true }); + customSound.play('ringtone', { loop: true, volume: callRingerVolume / 100 }); } if (isCalling) { - customSound.play('dialtone', { loop: true }); + customSound.play('dialtone', { loop: true, volume: callRingerVolume / 100 }); } return (): void => { customSound.stop('ringtone'); customSound.stop('dialtone'); }; - }, [customSound, isRinging, isCalling]); + }, [customSound, isRinging, isCalling, callRingerVolume]); return ( <> diff --git a/apps/meteor/server/methods/saveUserPreferences.ts b/apps/meteor/server/methods/saveUserPreferences.ts index 9f35abfc1440..ac124562fa65 100644 --- a/apps/meteor/server/methods/saveUserPreferences.ts +++ b/apps/meteor/server/methods/saveUserPreferences.ts @@ -27,7 +27,7 @@ type UserPreferences = { unreadAlert: boolean; masterVolume: number; notificationsSoundVolume: number; - voipRingerVolume: number; + callRingerVolume: number; desktopNotifications: string; pushNotifications: string; enableAutoAway: boolean; @@ -101,7 +101,7 @@ export const saveUserPreferences = async (settings: Partial, us unreadAlert: Match.Optional(Boolean), masterVolume: Match.Optional(Number), notificationsSoundVolume: Match.Optional(Number), - voipRingerVolume: Match.Optional(Number), + callRingerVolume: Match.Optional(Number), desktopNotifications: Match.Optional(String), pushNotifications: Match.Optional(String), enableAutoAway: Match.Optional(Boolean), diff --git a/apps/meteor/server/settings/accounts.ts b/apps/meteor/server/settings/accounts.ts index b2c51b797875..b1e061866720 100644 --- a/apps/meteor/server/settings/accounts.ts +++ b/apps/meteor/server/settings/accounts.ts @@ -711,7 +711,7 @@ export const createAccountSettings = () => i18nLabel: 'Notification_volume', }); - await this.add('Accounts_Default_User_Preferences_voipRingerVolume', 100, { + await this.add('Accounts_Default_User_Preferences_callRingerVolume', 100, { type: 'int', public: true, i18nLabel: 'Call_ringer_volume', diff --git a/apps/meteor/tests/e2e/page-objects/account-profile.ts b/apps/meteor/tests/e2e/page-objects/account-profile.ts index c1493a077fa4..73183bf49ad7 100644 --- a/apps/meteor/tests/e2e/page-objects/account-profile.ts +++ b/apps/meteor/tests/e2e/page-objects/account-profile.ts @@ -28,6 +28,7 @@ export class AccountProfile { return this.page.locator('//label[contains(text(), "Username")]/..//input'); } + // TODO: remove this locator get btnSubmit(): Locator { return this.page.locator('[data-qa="AccountProfilePageSaveButton"]'); } @@ -44,6 +45,14 @@ export class AccountProfile { return this.page.locator('//label[contains(text(), "Email")]/..//input'); } + get preferencesSoundAccordionOption(): Locator { + return this.page.locator('h2:has-text("Sound")'); + } + + get preferencesCallRingerVolumeSlider(): Locator { + return this.page.getByRole('slider', { name: 'Call Ringer Volume' }); + } + get btnClose(): Locator { return this.page.locator('role=navigation >> role=button[name=Close]'); } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 2a66a899f3ce..28e6aaa88fb4 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -353,6 +353,10 @@ export class HomeContent { return this.page.locator('#video-conf-root .rcx-button--primary.rcx-button >> text="Start call"'); } + getIncomingCallByName(name: string): Locator { + return this.page.getByRole('dialog', { name }); + } + get btnDeclineVideoCall(): Locator { return this.page.locator('.rcx-button--secondary-danger.rcx-button >> text="Decline"'); } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts index 5278e03c29f0..7b6a67ced7cd 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts @@ -68,6 +68,10 @@ export class HomeSidenav { return this.page.locator('role=menuitemcheckbox[name="Profile"]'); } + get accountPreferencesOption(): Locator { + return this.page.locator('role=menuitemcheckbox[name="Preferences"]'); + } + // TODO: refactor getSidebarItemByName to not use data-qa getSidebarItemByName(name: string, isRead?: boolean): Locator { return this.page.locator( diff --git a/apps/meteor/tests/e2e/page-objects/home-channel.ts b/apps/meteor/tests/e2e/page-objects/home-channel.ts index 0850c708a3c5..afbf824fe793 100644 --- a/apps/meteor/tests/e2e/page-objects/home-channel.ts +++ b/apps/meteor/tests/e2e/page-objects/home-channel.ts @@ -73,6 +73,14 @@ export class HomeChannel { return this.page.locator('role=menuitem[name="Mark Unread"]'); } + get audioVideoConfRingtone(): Locator { + return this.page.locator('#custom-sound-ringtone'); + } + + get audioVideoConfDialtone(): Locator { + return this.page.locator('#custom-sound-dialtone'); + } + get dialogEnterE2EEPassword(): Locator { return this.page.getByRole('dialog', { name: 'Enter E2EE password' }); } diff --git a/apps/meteor/tests/e2e/video-conference-ring.spec.ts b/apps/meteor/tests/e2e/video-conference-ring.spec.ts index 0b26c16cffc6..cd117b9adb83 100644 --- a/apps/meteor/tests/e2e/video-conference-ring.spec.ts +++ b/apps/meteor/tests/e2e/video-conference-ring.spec.ts @@ -3,33 +3,35 @@ import type { Page } from '@playwright/test'; import { IS_EE } from './config/constants'; import { createAuxContext } from './fixtures/createAuxContext'; import { Users } from './fixtures/userStates'; -import { HomeChannel } from './page-objects'; +import { HomeChannel, AccountProfile } from './page-objects'; import { expect, test } from './utils/test'; 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'); }); - let auxContext: { page: Page; poHomeChannel: HomeChannel }; + let auxContext: { page: Page; poHomeChannel: HomeChannel; poAccountProfile: AccountProfile }; test.beforeEach(async ({ browser }) => { const { page } = await createAuxContext(browser, Users.user2); - auxContext = { page, poHomeChannel: new HomeChannel(page) }; + auxContext = { page, poHomeChannel: new HomeChannel(page), poAccountProfile: new AccountProfile(page) }; }); test.afterEach(async () => { await auxContext.page.close(); }); - test('expect is ringing in direct', async () => { + test('should show call is ringing in direct', async () => { await poHomeChannel.sidenav.openChat('user2'); await auxContext.poHomeChannel.sidenav.openChat('user1'); @@ -41,7 +43,40 @@ test.describe('video conference ringing', () => { await expect(auxContext.poHomeChannel.content.videoConfRingCallText('Incoming call from')).toBeVisible(); await auxContext.poHomeChannel.content.btnDeclineVideoCall.click(); + }); - await auxContext.page.close(); + 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.getIncomingCallByName('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/apps/meteor/tests/end-to-end/api/miscellaneous.ts b/apps/meteor/tests/end-to-end/api/miscellaneous.ts index 69832805d8ea..0ab759c2ed20 100644 --- a/apps/meteor/tests/end-to-end/api/miscellaneous.ts +++ b/apps/meteor/tests/end-to-end/api/miscellaneous.ts @@ -163,7 +163,7 @@ describe('miscellaneous', () => { 'unreadAlert', 'masterVolume', 'notificationsSoundVolume', - 'voipRingerVolume', + 'callRingerVolume', 'omnichannelTranscriptEmail', IS_EE ? 'omnichannelTranscriptPDF' : false, 'desktopNotifications', diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 6478106c15d7..20bda171cf32 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -934,7 +934,7 @@ "Call_transfered_to__name__": "Call transfered to {{name}}", "Call_terminated": "Call terminated", "Call_ringer_volume": "Call ringer volume", - "Call_ringer_volume_hint": "For all incoming call notifications", + "Call_ringer_volume_hint": "For all incoming voice and video call notifications", "Caller": "Caller", "Caller_Id": "Caller ID", "Camera_access_not_allowed": "Camera access was not allowed, please check your browser settings.", @@ -2814,6 +2814,7 @@ "Incoming_call": "Incoming call", "Incoming_call_transfer": "Incoming call transfer", "Incoming_call_from": "Incoming call from", + "Incoming_call_from__roomName__": "Incoming call from {{roomName}}", "Incoming_Livechats": "Queued chats", "Incoming_WebHook": "Incoming WebHook", "Industry": "Industry", diff --git a/packages/i18n/src/locales/pt-BR.i18n.json b/packages/i18n/src/locales/pt-BR.i18n.json index a0ec01880a8f..af12fd9638c8 100644 --- a/packages/i18n/src/locales/pt-BR.i18n.json +++ b/packages/i18n/src/locales/pt-BR.i18n.json @@ -769,7 +769,7 @@ "Call_transfered_to__name__": "Chamada transferida para {{name}}", "Call_terminated": "Chamada encerrada", "Call_ringer_volume": "Volume do toque de chamada", - "Call_ringer_volume_hint": "Para todas as notificações de chamadas recebidas", + "Call_ringer_volume_hint": "Para todas as notificações de chamadas de voz e vídeo recebidas", "Caller": "Autor da chamada", "Caller_Id": "ID do autor da chamada", "Cancel": "Cancelar", diff --git a/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts b/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts index 01163d9aa4f7..98c72550e36e 100644 --- a/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts +++ b/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts @@ -21,7 +21,7 @@ export type UsersSetPreferencesParamsPOST = { unreadAlert?: boolean; masterVolume?: number; notificationsSoundVolume?: number; - voipRingerVolume?: number; + callRingerVolume?: number; desktopNotifications?: string; pushNotifications?: string; enableAutoAway?: boolean; @@ -120,7 +120,7 @@ const UsersSetPreferencesParamsPostSchema = { type: 'number', nullable: true, }, - voipRingerVolume: { + callRingerVolume: { type: 'number', nullable: true, }, diff --git a/packages/ui-voip/src/hooks/useVoipSounds.ts b/packages/ui-voip/src/hooks/useVoipSounds.ts index f08ff00e3da5..c42b47100ca7 100644 --- a/packages/ui-voip/src/hooks/useVoipSounds.ts +++ b/packages/ui-voip/src/hooks/useVoipSounds.ts @@ -6,8 +6,8 @@ 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); + const callRingerVolume = useUserPreference('callRingerVolume', 100) || 100; + const audioVolume = Math.floor((callRingerVolume * masterVolume) / 100); return useMemo( () => ({