diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-audio-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-audio-manager.js index 21ec88e1da5..542c38d9c9a 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat-audio-manager.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat-audio-manager.js @@ -37,11 +37,13 @@ export default class ChatAudioManager extends Service { } async play(soundName) { + if (isTesting()) { + return; + } + const audio = this._audioCache[soundName] || this._audioCache[DEFAULT_SOUND_NAME]; - audio.muted = isTesting(); - if (!audio.paused) { audio.pause(); if (typeof audio.fastSeek === "function") { diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-channel-notification-sound.js b/plugins/chat/assets/javascripts/discourse/services/chat-channel-notification-sound.js new file mode 100644 index 00000000000..1969e0e1788 --- /dev/null +++ b/plugins/chat/assets/javascripts/discourse/services/chat-channel-notification-sound.js @@ -0,0 +1,51 @@ +import Service, { service } from "@ember/service"; + +export default class ChatChannelNotificationSound extends Service { + @service chat; + @service chatAudioManager; + @service currentUser; + @service site; + + async play(channel) { + if (channel.isCategoryChannel) { + return false; + } + + if (channel.chatable.group) { + return false; + } + + if (this.currentUser.isInDoNotDisturb()) { + return false; + } + + if (!this.currentUser.chat_sound) { + return false; + } + + if (this.site.mobileView) { + return false; + } + + const membership = channel.currentUserMembership; + if (!membership.following) { + return false; + } + + if (membership.desktopNotificationLevel !== "always") { + return false; + } + + if (membership.muted) { + return false; + } + + if (this.chat.activeChannel === channel) { + return false; + } + + await this.chatAudioManager.play(this.currentUser.chat_sound); + + return true; + } +} diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js b/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js index bb6f009ade1..8b11d3d9a16 100644 --- a/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js +++ b/plugins/chat/assets/javascripts/discourse/services/chat-subscriptions-manager.js @@ -7,6 +7,7 @@ import ChatChannelArchive from "../models/chat-channel-archive"; export default class ChatSubscriptionsManager extends Service { @service store; @service chatChannelsManager; + @service chatChannelNotificationSound; @service chatTrackingStateManager; @service currentUser; @service appEvents; @@ -205,6 +206,8 @@ export default class ChatSubscriptionsManager extends Service { channel.tracking.unreadCount++; } + this.chatChannelNotificationSound.play(channel); + // Thread should be considered unread if not already. if (busData.thread_id && channel.threadingEnabled) { channel.threadsManager diff --git a/plugins/chat/test/javascripts/unit/services/chat-channel-notification-sound-test.js b/plugins/chat/test/javascripts/unit/services/chat-channel-notification-sound-test.js new file mode 100644 index 00000000000..d0947b13e42 --- /dev/null +++ b/plugins/chat/test/javascripts/unit/services/chat-channel-notification-sound-test.js @@ -0,0 +1,119 @@ +import { getOwner } from "@ember/application"; +import { test } from "qunit"; +import { + acceptance, + updateCurrentUser, +} from "discourse/tests/helpers/qunit-helpers"; +import ChatFabricators from "discourse/plugins/chat/discourse/lib/fabricators"; + +function buildDirectMessageChannel(owner) { + const channel = new ChatFabricators(owner).directMessageChannel(); + buildMembership(channel); + return channel; +} +function buildCategoryMessageChannel(owner) { + const channel = new ChatFabricators(owner).channel(); + buildMembership(channel); + return channel; +} + +function buildMembership(channel) { + channel.currentUserMembership = { + following: true, + desktop_notification_level: "always", + muted: false, + }; + return channel; +} + +acceptance( + "Discourse Chat | Unit | Service | chat-channel-notification-sound", + function (needs) { + needs.hooks.beforeEach(function () { + Object.defineProperty(this, "subject", { + get: () => + this.container.lookup("service:chat-channel-notification-sound"), + }); + + Object.defineProperty(this, "site", { + get: () => this.container.lookup("service:site"), + }); + + Object.defineProperty(this, "chat", { + get: () => this.container.lookup("service:chat"), + }); + + updateCurrentUser({ chat_sound: "ding" }); + }); + + needs.user(); + + test("in do not disturb", async function (assert) { + updateCurrentUser({ do_not_disturb_until: moment().add(1, "hour") }); + const channel = buildDirectMessageChannel(getOwner(this)); + + assert.deepEqual(await this.subject.play(channel), false); + }); + + test("no chat sound", async function (assert) { + updateCurrentUser({ chat_sound: null }); + const channel = buildDirectMessageChannel(getOwner(this)); + + assert.deepEqual(await this.subject.play(channel), false); + }); + + test("mobile", async function (assert) { + const channel = buildDirectMessageChannel(getOwner(this)); + this.site.mobileView = true; + + assert.deepEqual(await this.subject.play(channel), false); + }); + + test("plays sound", async function (assert) { + const channel = buildDirectMessageChannel(getOwner(this)); + + assert.deepEqual(await this.subject.play(channel), true); + }); + + test("muted", async function (assert) { + const channel = buildDirectMessageChannel(getOwner(this)); + channel.currentUserMembership.muted = true; + + assert.deepEqual(await this.subject.play(channel), false); + }); + + test("not following", async function (assert) { + const channel = buildDirectMessageChannel(getOwner(this)); + channel.currentUserMembership.following = false; + + assert.deepEqual(await this.subject.play(channel), false); + }); + + test("no notification", async function (assert) { + const channel = buildDirectMessageChannel(getOwner(this)); + channel.currentUserMembership.desktopNotificationLevel = "never"; + + assert.deepEqual(await this.subject.play(channel), false); + }); + + test("currently active channel", async function (assert) { + const channel = buildDirectMessageChannel(getOwner(this)); + this.chat.activeChannel = channel; + + assert.deepEqual(await this.subject.play(channel), false); + }); + + test("category channel", async function (assert) { + const channel = buildCategoryMessageChannel(getOwner(this)); + + assert.deepEqual(await this.subject.play(channel), false); + }); + + test("group", async function (assert) { + const channel = buildDirectMessageChannel(getOwner(this)); + channel.chatable.group = true; + + assert.deepEqual(await this.subject.play(channel), false); + }); + } +);