diff --git a/addons/mail/static/src/discuss/call/common/rtc_service.js b/addons/mail/static/src/discuss/call/common/rtc_service.js index 3c6c517e301..9cbda23e5fa 100644 --- a/addons/mail/static/src/discuss/call/common/rtc_service.js +++ b/addons/mail/static/src/discuss/call/common/rtc_service.js @@ -1553,7 +1553,15 @@ export class Rtc { return; } const oldCount = channel.rtcSessions.length; + const hadSelfSession = Boolean(this.state.selfSession?.in(channel.rtcSessions)); channel.rtcSessions = sessionsData; + if ( + !hadSelfSession || + hadSelfSession !== Boolean(this.state.selfSession?.in(channel.rtcSessions)) || + !this.store.env.services["multi_tab"].isOnMainTab() + ) { + return; + } if (channel.rtcSessions.length > oldCount) { this.soundEffectsService.play("channel-join"); } else if (channel.rtcSessions.length < oldCount) { diff --git a/addons/mail/static/tests/discuss/call/call_tests.js b/addons/mail/static/tests/discuss/call/call_tests.js index 21cb72b39ed..d0114ef8ffe 100644 --- a/addons/mail/static/tests/discuss/call/call_tests.js +++ b/addons/mail/static/tests/discuss/call/call_tests.js @@ -105,16 +105,6 @@ QUnit.test("should disconnect when closing page while in call", async (assert) = }); QUnit.test("should display invitations", async (assert) => { - patchWithCleanup(browser, { - Audio: class extends Audio { - pause() { - assert.step("pause_sound_effect"); - } - play() { - assert.step("play_sound_effect"); - } - }, - }); const pyEnv = await startServer(); const channelId = pyEnv["discuss.channel"].create({ name: "General" }); const partnerId = pyEnv["res.partner"].create({ name: "InvitationSender" }); @@ -126,7 +116,17 @@ QUnit.test("should display invitations", async (assert) => { channel_member_id: memberId, channel_id: channelId, }); - await start(); + const { env } = await start(); + patchWithCleanup(env.services["mail.sound_effects"], { + play(name) { + assert.step(`play - ${name}`); + super.play(...arguments); + }, + stop(name) { + assert.step(`stop - ${name}`); + super.stop(...arguments); + }, + }); pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", { Thread: { id: channelId, @@ -135,7 +135,7 @@ QUnit.test("should display invitations", async (assert) => { }, }); await contains(".o-discuss-CallInvitation"); - assert.verifySteps(["play_sound_effect"]); + assert.verifySteps(["play - incoming-call"]); // Simulate stop receiving call invitation pyEnv["bus.bus"]._sendone(pyEnv.currentPartner, "mail.record/insert", { Thread: { @@ -145,7 +145,7 @@ QUnit.test("should display invitations", async (assert) => { }, }); await contains(".o-discuss-CallInvitation", { count: 0 }); - assert.verifySteps(["pause_sound_effect"]); + assert.verifySteps(["stop - incoming-call"]); }); QUnit.test("can share screen", async () => { @@ -223,3 +223,31 @@ QUnit.test("Click on inset card should replace the inset and active stream toget await contains("video[type='screen'].o-inset"); await contains("video[type='camera']:not(.o-inset)"); }); + +QUnit.test("join/leave sounds are only played on main tab", async (assert) => { + mockGetMedia(); + const pyEnv = await startServer(); + const channelId = pyEnv["discuss.channel"].create({ name: "General" }); + const tab1 = await start({ asTab: true }); + const tab2 = await start({ asTab: true }); + patchWithCleanup(tab1.env.services["mail.sound_effects"], { + play(name) { + assert.step(`tab1 - play - ${name}`); + }, + }); + patchWithCleanup(tab2.env.services["mail.sound_effects"], { + play(name) { + assert.step(`tab2 - play - ${name}`); + }, + }); + await tab1.openDiscuss(channelId); + await tab2.openDiscuss(channelId); + await click("[title='Start a Call']", { target: tab1.target }); + await contains(".o-discuss-Call", { target: tab1.target }); + await contains(".o-discuss-Call", { target: tab2.target }); + assert.verifySteps(["tab1 - play - channel-join"]); + await click("[title='Disconnect']:not([disabled])", { target: tab1.target }); + await contains(".o-discuss-Call", { target: tab1.target, count: 0 }); + await contains(".o-discuss-Call", { target: tab2.target, count: 0 }); + assert.verifySteps(["tab1 - play - channel-leave"]); +}); diff --git a/addons/mail/static/tests/helpers/mock_server/controllers/discuss.js b/addons/mail/static/tests/helpers/mock_server/controllers/discuss.js index 4d8d970996a..f473506e02e 100644 --- a/addons/mail/static/tests/helpers/mock_server/controllers/discuss.js +++ b/addons/mail/static/tests/helpers/mock_server/controllers/discuss.js @@ -167,7 +167,7 @@ patch(MockServer.prototype, { return this._mockRouteMailRtcChannelLeaveCall(args.channel_id); } if (route === "/mail/rtc/session/update_and_broadcast") { - return; + return this._mockRouteMailRtcSessionUpdateAndBroadcast(args.session_id, args.values); } if (route === "/mail/starred/messages") { const { search_term, after, before, limit } = args; @@ -602,6 +602,23 @@ patch(MockServer.prototype, { } this.pyEnv["bus.bus"]._sendmany(notifications); }, + /** + * Simulates the `/mail/rtc/session/update_and_broadcast` route. + * + * @param {number} session_id + * @param {object} values + */ + async _mockRouteMailRtcSessionUpdateAndBroadcast(session_id, values) { + const [session] = this.pyEnv["discuss.channel.rtc.session"].searchRead([ + ["id", "=", session_id], + ]); + const [currentChannelMember] = this.pyEnv["discuss.channel.member"].searchRead([ + ["id", "=", session.channel_member_id[0]], + ]); + if (session && currentChannelMember.partner_id[0] === this.pyEnv.currentPartnerId) { + this._mockDiscussChannelRtcSession__updateAndBroadcast(session.id, values); + } + }, /** * Simulates the `/mail/thread/data` route. * diff --git a/addons/mail/static/tests/helpers/mock_server/models/discuss_channel_rtc_session.js b/addons/mail/static/tests/helpers/mock_server/models/discuss_channel_rtc_session.js index 2de86e67305..82ef578581b 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/discuss_channel_rtc_session.js +++ b/addons/mail/static/tests/helpers/mock_server/models/discuss_channel_rtc_session.js @@ -4,6 +4,30 @@ import { patch } from "@web/core/utils/patch"; import { MockServer } from "@web/../tests/helpers/mock_server"; patch(MockServer.prototype, { + mockCreate(model) { + if (model !== "discuss.channel.rtc.session") { + return super.mockCreate(...arguments); + } + const sessionIds = super.mockCreate(...arguments); + const channelInfo = + this._mockDiscussChannelRtcSession_DiscussChannelRtcSessionFormatByChannel(sessionIds); + const notifications = []; + for (const [channelId, sessionData] of Object.entries(channelInfo)) { + const [channel] = this.pyEnv["discuss.channel"].searchRead([ + ["id", "=", Number(channelId)], + ]); + notifications.push([ + channel, + "discuss.channel/rtc_sessions_update", + { + id: channel.id, + rtcSessions: [["ADD", sessionData]], + }, + ]); + } + this.pyEnv["bus.bus"]._sendmany(notifications); + return sessionIds; + }, /** * Simulates `_mail_rtc_session_format` on `discuss.channel.rtc.session`. * @@ -54,4 +78,21 @@ patch(MockServer.prototype, { } return data; }, + /** + * Simulates `_update_and_broadcast` on `discuss.channel.rtc.session`. + * + * @param {object} values + */ + _mockDiscussChannelRtcSession__updateAndBroadcast(id, values) { + this.pyEnv["discuss.channel.rtc.session"].write([id], values); + const sessionData = this._mockDiscussChannelRtcSession_DiscussChannelRtcSessionFormat(id); + const [channel] = this.pyEnv["discuss.channel"].searchRead([ + ["id", "=", sessionData.channelMember.thread.id], + ]); + this.pyEnv["bus.bus"]._sendone( + channel, + "discuss.channel.rtc.session/update_and_broadcast", + { data: sessionData, channelId: channel.id } + ); + }, });