diff --git a/app/assets/javascripts/discourse/app/lib/utilities.js b/app/assets/javascripts/discourse/app/lib/utilities.js index 13f5ef07039..8443ba25d4a 100644 --- a/app/assets/javascripts/discourse/app/lib/utilities.js +++ b/app/assets/javascripts/discourse/app/lib/utilities.js @@ -769,3 +769,19 @@ export function cleanNullQueryParams(params) { export function getElement(node) { return node.nodeType === Node.TEXT_NODE ? node.parentElement : node; } + +export function isPrimaryTab() { + return new Promise((resolve) => { + if (capabilities.supportsServiceWorker) { + navigator.serviceWorker.addEventListener("message", (event) => { + resolve(event.data.primaryTab); + }); + + navigator.serviceWorker.ready.then((registration) => { + registration.active.postMessage({ action: "primaryTab" }); + }); + } else { + resolve(true); + } + }); +} diff --git a/app/assets/javascripts/discourse/app/services/capabilities.js b/app/assets/javascripts/discourse/app/services/capabilities.js index 565474d99e2..37b40bd8d2f 100644 --- a/app/assets/javascripts/discourse/app/services/capabilities.js +++ b/app/assets/javascripts/discourse/app/services/capabilities.js @@ -43,6 +43,16 @@ class Capabilities { !("userActivation" in navigator) || navigator.userActivation.hasBeenActive ); } + + get supportsServiceWorker() { + return ( + "serviceWorker" in navigator && + typeof ServiceWorkerRegistration !== "undefined" && + !this.isAppWebview && + navigator.serviceWorker.controller && + navigator.serviceWorker.controller.state === "activated" + ); + } } export const capabilities = new Capabilities(); diff --git a/app/assets/javascripts/service-worker.js.erb b/app/assets/javascripts/service-worker.js.erb index 6289572dcc3..3431658c23c 100644 --- a/app/assets/javascripts/service-worker.js.erb +++ b/app/assets/javascripts/service-worker.js.erb @@ -99,7 +99,6 @@ self.addEventListener('notificationclick', function(event) { } }); - self.addEventListener('pushsubscriptionchange', function(event) { event.waitUntil( Promise.all( @@ -126,6 +125,22 @@ self.addEventListener('pushsubscriptionchange', function(event) { ); }); +self.addEventListener('message', function(event) { + if (event.data?.action !== "primaryTab") { + return; + } + + event.waitUntil( + self.clients.matchAll().then(function(clients) { + clients.forEach(function(client) { + client.postMessage({ + primaryTab: client.focused + }); + }); + }) + ); + }); + <% DiscoursePluginRegistry.service_workers.each do |js| %> <%=raw "#{File.read(js)}" %> <% end %> diff --git a/plugins/chat/assets/javascripts/discourse/initializers/chat-audio.js b/plugins/chat/assets/javascripts/discourse/initializers/chat-audio.js index cd64b85c099..cc3bf623c2c 100644 --- a/plugins/chat/assets/javascripts/discourse/initializers/chat-audio.js +++ b/plugins/chat/assets/javascripts/discourse/initializers/chat-audio.js @@ -1,4 +1,5 @@ import { withPluginApi } from "discourse/lib/plugin-api"; +import { isPrimaryTab } from "discourse/lib/utilities"; import { INDICATOR_PREFERENCES } from "discourse/plugins/chat/discourse/lib/chat-constants"; const MENTION = 29; @@ -15,6 +16,10 @@ export default { return; } + this.canPlaySound = async () => { + return await isPrimaryTab(); + }; + withPluginApi("0.12.1", (api) => { api.registerDesktopNotificationHandler((data, siteSettings, user) => { const indicatorType = user.user_option.chat_header_indicator_preference; @@ -24,10 +29,7 @@ export default { return; } - if ( - !user.user_option.chat_sound || - indicatorType === INDICATOR_PREFERENCES.never - ) { + if (!user.chat_sound || indicatorType === INDICATOR_PREFERENCES.never) { return; } @@ -47,10 +49,14 @@ export default { } if (CHAT_NOTIFICATION_TYPES.includes(data.notification_type)) { - const chatAudioManager = container.lookup( - "service:chat-audio-manager" - ); - chatAudioManager.play(user.chat_sound); + this.canPlaySound().then((success) => { + if (success) { + const chatAudioManager = container.lookup( + "service:chat-audio-manager" + ); + chatAudioManager.play(user.chat_sound); + } + }); } }); }); diff --git a/plugins/chat/test/javascripts/unit/lib/chat-audio-test.js b/plugins/chat/test/javascripts/unit/lib/chat-audio-test.js index f51ade2eeaf..301b16b4c81 100644 --- a/plugins/chat/test/javascripts/unit/lib/chat-audio-test.js +++ b/plugins/chat/test/javascripts/unit/lib/chat-audio-test.js @@ -19,14 +19,19 @@ module("Discourse Chat | Unit | chat-audio", function (hooks) { this.siteSettings = getOwner(this).lookup("service:site-settings"); this.siteSettings.chat_enabled = true; + this.currentUser.chat_sound = "ding"; this.currentUser.user_option.has_chat_enabled = true; - this.currentUser.user_option.chat_sound = "ding"; this.currentUser.user_option.chat_header_indicator_preference = "all_new"; withPluginApi("0.12.1", async (api) => { this.stub = sinon.spy(api, "registerDesktopNotificationHandler"); chatAudioInitializer.initialize(getOwner(this)); + // stub the service worker response + sinon + .stub(chatAudioInitializer, "canPlaySound") + .returns(Promise.resolve(true)); + this.notificationHandler = this.stub.getCall(0).callback; this.playStub = sinon.stub(chatAudioManager, "play"); @@ -43,58 +48,65 @@ module("Discourse Chat | Unit | chat-audio", function (hooks) { assert.ok(this.stub.calledOnce); }); - test("it plays chat sound", function (assert) { - this.handleNotification(); + test("it plays chat sound", async function (assert) { + await this.handleNotification(); assert.ok(this.playStub.calledOnce); }); - test("it skips chat sound for user in DND mode", function (assert) { + test("it skips chat sound for user in DND mode", async function (assert) { this.currentUser.isInDoNotDisturb = () => true; - this.handleNotification(); + await this.handleNotification(); assert.ok(this.playStub.notCalled); }); - test("it skips chat sound for user with no chat sound set", function (assert) { - this.currentUser.user_option.chat_sound = null; - this.handleNotification(); + test("it skips chat sound for user with no chat sound set", async function (assert) { + this.currentUser.chat_sound = null; + await this.handleNotification(); assert.ok(this.playStub.notCalled); }); - test("it plays a chat sound for mentions", function (assert) { + test("it plays a chat sound for mentions", async function (assert) { this.currentUser.user_option.chat_header_indicator_preference = "only_mentions"; - this.handleNotification({ notification_type: 29 }); + await this.handleNotification({ notification_type: 29 }); assert.ok(this.playStub.calledOnce); }); - test("it skips chat sound for non-mentions", function (assert) { + test("it skips chat sound for non-mentions", async function (assert) { this.currentUser.user_option.chat_header_indicator_preference = "only_mentions"; - this.handleNotification(); + await this.handleNotification(); assert.ok(this.playStub.notCalled); }); - test("it plays a chat sound for DMs", function (assert) { + test("it plays a chat sound for DMs", async function (assert) { this.currentUser.user_option.chat_header_indicator_preference = "dm_and_mentions"; - this.handleNotification({ is_direct_message_channel: true }); + await this.handleNotification({ is_direct_message_channel: true }); assert.ok(this.playStub.calledOnce); }); - test("it skips chat sound for non-DM messages", function (assert) { + test("it skips chat sound for non-DM messages", async function (assert) { this.currentUser.user_option.chat_header_indicator_preference = "dm_and_mentions"; - this.handleNotification({ is_direct_message_channel: false }); + await this.handleNotification({ is_direct_message_channel: false }); + + assert.ok(this.playStub.notCalled); + }); + + test("it skips chat sound when service worker returns false", async function (assert) { + chatAudioInitializer.canPlaySound.returns(Promise.resolve(false)); + await this.handleNotification(); assert.ok(this.playStub.notCalled); });