diff --git a/app/javascript/dashboard/helper/AudioAlerts/DashboardAudioNotificationHelper.js b/app/javascript/dashboard/helper/AudioAlerts/DashboardAudioNotificationHelper.js new file mode 100644 index 000000000..66bfa4cd4 --- /dev/null +++ b/app/javascript/dashboard/helper/AudioAlerts/DashboardAudioNotificationHelper.js @@ -0,0 +1,100 @@ +import { MESSAGE_TYPE } from 'shared/constants/messages'; +import { showBadgeOnFavicon } from './faviconHelper'; +import { initFaviconSwitcher } from './faviconHelper'; +import { + getAlertAudio, + initOnEvents, +} from 'shared/helpers/AudioNotificationHelper'; + +class DashboardAudioNotificationHelper { + constructor() { + this.recurringNotificationTimer = null; + this.audioAlertType = 'none'; + this.playAlertOnlyWhenHidden = true; + this.currentUserId = null; + this.audioAlertTone = 'ding'; + } + + setInstanceValues = ({ + currentUserId, + alwaysPlayAudioAlert, + audioAlertType, + audioAlertTone, + }) => { + this.audioAlertType = audioAlertType; + this.playAlertOnlyWhenHidden = !alwaysPlayAudioAlert; + this.currentUserId = currentUserId; + this.audioAlertTone = audioAlertTone; + initOnEvents.forEach(e => { + document.addEventListener(e, this.onAudioListenEvent, false); + }); + initFaviconSwitcher(); + }; + + onAudioListenEvent = async () => { + try { + await getAlertAudio('', { + type: 'dashboard', + alertTone: this.audioAlertTone, + }); + initOnEvents.forEach(event => { + document.removeEventListener(event, this.onAudioListenEvent, false); + }); + } catch (error) { + // Ignore audio fetch errors + } + }; + + isConversationAssignedToCurrentUser = message => { + const conversationAssigneeId = message?.conversation?.assignee_id; + return conversationAssigneeId === this.currentUserId; + }; + + isMessageFromCurrentConversation = message => { + return ( + window.WOOT.$store.getters.getSelectedChat?.id === message.conversation_id + ); + }; + + isMessageFromCurrentUser = message => { + return message?.sender_id === this.currentUserId; + }; + + shouldNotifyOnMessage = message => { + if (this.audioAlertType === 'mine') { + return this.isConversationAssignedToCurrentUser(message); + } + return this.audioAlertType === 'all'; + }; + + onNewMessage = message => { + // If the message is sent by the current user or the + // correct notification is not enabled, then dismiss the alert + if ( + this.isMessageFromCurrentUser(message) || + !this.shouldNotifyOnMessage(message) + ) { + return; + } + + // If the message type is not incoming or private, then dismiss the alert + const { message_type: messageType, private: isPrivate } = message; + if (messageType !== MESSAGE_TYPE.INCOMING && !isPrivate) { + return; + } + + // If the user looking at the conversation, then dismiss the alert + if (this.isMessageFromCurrentConversation(message) && !document.hidden) { + return; + } + // If the user has disabled alerts when active on the dashboard, the dismiss the alert + if (this.playAlertOnlyWhenHidden && !document.hidden) { + return; + } + + window.playAudioAlert(); + showBadgeOnFavicon(); + }; +} + +export default new DashboardAudioNotificationHelper(); diff --git a/app/javascript/shared/helpers/faviconHelper.js b/app/javascript/dashboard/helper/AudioAlerts/faviconHelper.js similarity index 100% rename from app/javascript/shared/helpers/faviconHelper.js rename to app/javascript/dashboard/helper/AudioAlerts/faviconHelper.js diff --git a/app/javascript/dashboard/helper/actionCable.js b/app/javascript/dashboard/helper/actionCable.js index edf84bee6..c1b82bc1c 100644 --- a/app/javascript/dashboard/helper/actionCable.js +++ b/app/javascript/dashboard/helper/actionCable.js @@ -1,6 +1,6 @@ import AuthAPI from '../api/auth'; import BaseActionCableConnector from '../../shared/helpers/BaseActionCableConnector'; -import { newMessageNotification } from 'shared/helpers/AudioNotificationHelper'; +import DashboardAudioNotificationHelper from './AudioAlerts/DashboardAudioNotificationHelper'; class ActionCableConnector extends BaseActionCableConnector { constructor(app, pubsubToken) { @@ -74,7 +74,7 @@ class ActionCableConnector extends BaseActionCableConnector { onLogout = () => AuthAPI.logout(); onMessageCreated = data => { - newMessageNotification(data); + DashboardAudioNotificationHelper.onNewMessage(data); this.app.$store.dispatch('addMessage', data); }; diff --git a/app/javascript/dashboard/helper/scriptHelpers.js b/app/javascript/dashboard/helper/scriptHelpers.js index 406602335..291769d2d 100644 --- a/app/javascript/dashboard/helper/scriptHelpers.js +++ b/app/javascript/dashboard/helper/scriptHelpers.js @@ -1,4 +1,5 @@ import AnalyticsHelper from './AnalyticsHelper'; +import DashboardAudioNotificationHelper from './AudioAlerts/DashboardAudioNotificationHelper'; export const CHATWOOT_SET_USER = 'CHATWOOT_SET_USER'; export const CHATWOOT_RESET = 'CHATWOOT_RESET'; @@ -13,6 +14,23 @@ export const initializeAnalyticsEvents = () => { window.bus.$on(ANALYTICS_RESET, () => {}); }; +const initializeAudioAlerts = user => { + // InitializeAudioNotifications + const { ui_settings: uiSettings } = user || {}; + const { + always_play_audio_alert: alwaysPlayAudioAlert, + enable_audio_alerts: audioAlertType, + notification_tone: audioAlertTone, + } = uiSettings; + + DashboardAudioNotificationHelper.setInstanceValues({ + currentUserId: user.id, + audioAlertType: audioAlertType || 'none', + audioAlertTone: audioAlertTone || 'ding', + alwaysPlayAudioAlert: alwaysPlayAudioAlert || false, + }); +}; + export const initializeChatwootEvents = () => { window.bus.$on(CHATWOOT_RESET, () => { if (window.$chatwoot) { @@ -32,5 +50,7 @@ export const initializeChatwootEvents = () => { cloudCustomer: 'true', }); } + + initializeAudioAlerts(user); }); }; diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 9f6c7ac30..e3cb16063 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -16,11 +16,6 @@ import App from '../dashboard/App'; import i18n from '../dashboard/i18n'; import createAxios from '../dashboard/helper/APIHelper'; import commonHelpers, { isJSONValid } from '../dashboard/helper/commons'; -import { - getAlertAudio, - initOnEvents, -} from '../shared/helpers/AudioNotificationHelper'; -import { initFaviconSwitcher } from '../shared/helpers/faviconHelper'; import router, { initalizeRouter } from '../dashboard/routes'; import store from '../dashboard/store'; import constants from '../dashboard/constants'; @@ -93,17 +88,6 @@ window.onload = () => { }).$mount('#app'); }; -const setupAudioListeners = () => { - getAlertAudio().then(() => { - initOnEvents.forEach(event => { - document.removeEventListener(event, setupAudioListeners, false); - }); - }); -}; window.addEventListener('load', () => { window.playAudioAlert = () => {}; - initOnEvents.forEach(e => { - document.addEventListener(e, setupAudioListeners, false); - }); - initFaviconSwitcher(); }); diff --git a/app/javascript/sdk/IFrameHelper.js b/app/javascript/sdk/IFrameHelper.js index 2556241dd..7a8efa04f 100644 --- a/app/javascript/sdk/IFrameHelper.js +++ b/app/javascript/sdk/IFrameHelper.js @@ -127,7 +127,7 @@ export const IFrameHelper = { setupAudioListeners: () => { const { baseUrl = '' } = window.$chatwoot; - getAlertAudio(baseUrl, 'widget').then(() => + getAlertAudio(baseUrl, { type: 'widget', alertTone: 'ding' }).then(() => initOnEvents.forEach(event => { document.removeEventListener( event, diff --git a/app/javascript/shared/helpers/AudioNotificationHelper.js b/app/javascript/shared/helpers/AudioNotificationHelper.js index 7474eb583..cf1cd8087 100644 --- a/app/javascript/shared/helpers/AudioNotificationHelper.js +++ b/app/javascript/shared/helpers/AudioNotificationHelper.js @@ -1,8 +1,3 @@ -import { MESSAGE_TYPE } from 'shared/constants/messages'; -import { IFrameHelper } from 'widget/helpers/utils'; - -import { showBadgeOnFavicon } from './faviconHelper'; - export const initOnEvents = ['click', 'touchstart', 'keypress', 'keydown']; export const getAudioContext = () => { @@ -15,19 +10,8 @@ export const getAudioContext = () => { return audioCtx; }; -const getAlertTone = alertType => { - if (alertType === 'dashboard') { - const { - notification_tone: tone, - } = window.WOOT.$store.getters.getUISettings; - return tone; - } - return 'ding'; -}; - -export const getAlertAudio = async (baseUrl = '', type = 'dashboard') => { +export const getAlertAudio = async (baseUrl = '', requestContext) => { const audioCtx = getAudioContext(); - const playSound = audioBuffer => { window.playAudioAlert = () => { if (audioCtx) { @@ -41,7 +25,7 @@ export const getAlertAudio = async (baseUrl = '', type = 'dashboard') => { }; if (audioCtx) { - const alertTone = getAlertTone(type); + const { type = 'dashboard', alertTone = 'ding' } = requestContext || {}; const resourceUrl = `${baseUrl}/audio/${type}/${alertTone}.mp3`; const audioRequest = new Request(resourceUrl); @@ -56,87 +40,3 @@ export const getAlertAudio = async (baseUrl = '', type = 'dashboard') => { }); } }; - -export const notificationEnabled = (enableAudioAlerts, id, userId) => { - if (enableAudioAlerts === 'mine') { - return userId === id; - } - if (enableAudioAlerts === 'all') { - return true; - } - return false; -}; - -export const shouldPlayAudio = ( - message, - conversationId, - userId, - isDocHidden -) => { - const { - conversation_id: incomingConvId, - sender_id: senderId, - message_type: messageType, - private: isPrivate, - } = message; - if (!isDocHidden && messageType === MESSAGE_TYPE.INCOMING) { - showBadgeOnFavicon(); - return false; - } - const isFromCurrentUser = userId === senderId; - - const playAudio = - !isFromCurrentUser && (messageType === MESSAGE_TYPE.INCOMING || isPrivate); - if (isDocHidden) return playAudio; - if (conversationId !== incomingConvId) return playAudio; - return false; -}; - -export const getAssigneeFromNotification = currentConv => { - let id; - if (currentConv.meta) { - const assignee = currentConv.meta.assignee; - if (assignee) { - id = assignee.id; - } - } - return id; -}; - -export const newMessageNotification = data => { - const { conversation_id: currentConvId } = window.WOOT.$route.params; - const currentUserId = window.WOOT.$store.getters.getCurrentUserID; - const { conversation_id: incomingConvId } = data; - const currentConv = - window.WOOT.$store.getters.getConversationById(incomingConvId) || {}; - const assigneeId = getAssigneeFromNotification(currentConv); - - const { - enable_audio_alerts: enableAudioAlerts = false, - always_play_audio_alert: alwaysPlayAudioAlert, - } = window.WOOT.$store.getters.getUISettings; - const isDocHidden = alwaysPlayAudioAlert ? true : document.hidden; - - const playAudio = shouldPlayAudio( - data, - currentConvId, - currentUserId, - isDocHidden - ); - const isNotificationEnabled = notificationEnabled( - enableAudioAlerts, - currentUserId, - assigneeId - ); - - if (playAudio && isNotificationEnabled) { - window.playAudioAlert(); - showBadgeOnFavicon(); - } -}; - -export const playNewMessageNotificationInWidget = () => { - IFrameHelper.sendMessage({ - event: 'playAudio', - }); -}; diff --git a/app/javascript/shared/helpers/specs/AudioNotificationHelper.spec.js b/app/javascript/shared/helpers/specs/AudioNotificationHelper.spec.js deleted file mode 100644 index 7dc935400..000000000 --- a/app/javascript/shared/helpers/specs/AudioNotificationHelper.spec.js +++ /dev/null @@ -1,151 +0,0 @@ -/** - * @jest-environment jsdom - */ - -import { - shouldPlayAudio, - notificationEnabled, - getAssigneeFromNotification, -} from '../AudioNotificationHelper'; - -describe('shouldPlayAudio', () => { - describe('Document active', () => { - it('Retuns true if incoming message', () => { - const message = { - conversation_id: 10, - sender_id: 5, - message_type: 0, - private: false, - }; - const [conversationId, userId, isDocHiddden] = [1, 2, true]; - const result = shouldPlayAudio( - message, - conversationId, - userId, - isDocHiddden - ); - expect(result).toBe(true); - }); - it('Retuns false if outgoing message', () => { - const message = { - conversation_id: 10, - sender_id: 5, - message_type: 1, - private: false, - }; - const [conversationId, userId, isDocHiddden] = [1, 2, false]; - const result = shouldPlayAudio( - message, - conversationId, - userId, - isDocHiddden - ); - expect(result).toBe(false); - }); - - it('Retuns false if from Same sender', () => { - const message = { - conversation_id: 1, - sender_id: 2, - message_type: 0, - private: false, - }; - const [conversationId, userId, isDocHiddden] = [1, 2, true]; - const result = shouldPlayAudio( - message, - conversationId, - userId, - isDocHiddden - ); - expect(result).toBe(false); - }); - it('Retuns true if private message from another agent', () => { - const message = { - conversation_id: 1, - sender_id: 5, - message_type: 1, - private: true, - }; - const [conversationId, userId, isDocHiddden] = [1, 2, true]; - const result = shouldPlayAudio( - message, - conversationId, - userId, - isDocHiddden - ); - expect(result).toBe(true); - }); - }); - describe('Document inactive', () => { - it('Retuns true if incoming message', () => { - const message = { - conversation_id: 1, - sender_id: 5, - message_type: 0, - private: false, - }; - const [conversationId, userId, isDocHiddden] = [1, 2, true]; - const result = shouldPlayAudio( - message, - conversationId, - userId, - isDocHiddden - ); - expect(result).toBe(true); - }); - it('Retuns false if outgoing message', () => { - const message = { - conversation_id: 1, - sender_id: 5, - message_type: 1, - private: false, - }; - const [conversationId, userId, isDocHiddden] = [1, 2, true]; - const result = shouldPlayAudio( - message, - conversationId, - userId, - isDocHiddden - ); - expect(result).toBe(false); - }); - }); -}); -describe('notificationEnabled', () => { - it('returns true if mine', () => { - const [enableAudioAlerts, userId, id] = ['mine', 1, 1]; - const result = notificationEnabled(enableAudioAlerts, userId, id); - expect(result).toBe(true); - }); - it('returns true if all', () => { - const [enableAudioAlerts, userId, id] = ['all', 1, 2]; - const result = notificationEnabled(enableAudioAlerts, userId, id); - expect(result).toBe(true); - }); - it('returns false if none', () => { - const [enableAudioAlerts, userId, id] = ['none', 1, 2]; - const result = notificationEnabled(enableAudioAlerts, userId, id); - expect(result).toBe(false); - }); -}); -describe('getAssigneeFromNotification', () => { - it('Retuns true if gets notification from assignee', () => { - const currentConv = { - id: 1, - accountId: 1, - meta: { - assignee: { - id: 1, - name: 'John', - }, - }, - }; - const result = getAssigneeFromNotification(currentConv); - expect(result).toBe(1); - }); - it('Retuns true if gets notification from assignee is udefined', () => { - const currentConv = {}; - const result = getAssigneeFromNotification(currentConv); - expect(result).toBe(undefined); - }); -}); diff --git a/app/javascript/widget/helpers/WidgetAudioNotificationHelper.js b/app/javascript/widget/helpers/WidgetAudioNotificationHelper.js new file mode 100644 index 000000000..3f1997f7b --- /dev/null +++ b/app/javascript/widget/helpers/WidgetAudioNotificationHelper.js @@ -0,0 +1,5 @@ +import { IFrameHelper } from 'widget/helpers/utils'; + +export const playNewMessageNotificationInWidget = () => { + IFrameHelper.sendMessage({ event: 'playAudio' }); +}; diff --git a/app/javascript/widget/helpers/actionCable.js b/app/javascript/widget/helpers/actionCable.js index 84edb35ba..4b7140bcf 100644 --- a/app/javascript/widget/helpers/actionCable.js +++ b/app/javascript/widget/helpers/actionCable.js @@ -1,5 +1,5 @@ import BaseActionCableConnector from '../../shared/helpers/BaseActionCableConnector'; -import { playNewMessageNotificationInWidget } from 'shared/helpers/AudioNotificationHelper'; +import { playNewMessageNotificationInWidget } from 'widget/helpers/WidgetAudioNotificationHelper'; import { ON_AGENT_MESSAGE_RECEIVED } from '../constants/widgetBusEvents'; class ActionCableConnector extends BaseActionCableConnector {