// Converse.js (A browser based XMPP chat client) // https://conversejs.org // // Copyright (c) 2013-2019, JC Brand // Licensed under the Mozilla Public License (MPLv2) // /*global define */ import converse from "@converse/headless/converse-core"; const { Strophe, _, sizzle } = converse.env, u = converse.env.utils; converse.plugins.add('converse-notification', { dependencies: ["converse-chatboxes"], initialize () { /* The initialize function gets called as soon as the plugin is * loaded by converse.js's plugin machinery. */ const { _converse } = this; const { __ } = _converse; _converse.supports_html5_notification = "Notification" in window; _converse.api.settings.update({ notify_all_room_messages: false, show_desktop_notifications: true, show_chatstate_notifications: false, chatstate_notification_blacklist: [], // ^ a list of JIDs to ignore concerning chat state notifications play_sounds: true, sounds_path: 'sounds/', notification_icon: 'logo/conversejs-filled.svg', notification_delay: 5000 }); _converse.isOnlyChatStateNotification = (msg) => // See XEP-0085 Chat State Notification _.isNull(msg.querySelector('body')) && ( _.isNull(msg.querySelector(_converse.ACTIVE)) || _.isNull(msg.querySelector(_converse.COMPOSING)) || _.isNull(msg.querySelector(_converse.INACTIVE)) || _.isNull(msg.querySelector(_converse.PAUSED)) || _.isNull(msg.querySelector(_converse.GONE)) ) ; _converse.shouldNotifyOfGroupMessage = function (message) { /* Is this a group message worthy of notification? */ let notify_all = _converse.notify_all_room_messages; const jid = message.getAttribute('from'), resource = Strophe.getResourceFromJid(jid), room_jid = Strophe.getBareJidFromJid(jid), sender = resource && Strophe.unescapeNode(resource) || ''; if (sender === '' || message.querySelectorAll('delay').length > 0) { return false; } const room = _converse.chatboxes.get(room_jid); const body = message.querySelector('body'); if (_.isNull(body)) { return false; } const mentioned = (new RegExp(`\\b${room.get('nick')}\\b`)).test(body.textContent); notify_all = notify_all === true || (_.isArray(notify_all) && _.includes(notify_all, room_jid)); if (sender === room.get('nick') || (!notify_all && !mentioned)) { return false; } return true; }; _converse.isMessageToHiddenChat = function (message) { if (_converse.isUniView()) { const jid = Strophe.getBareJidFromJid(message.getAttribute('from')), view = _converse.chatboxviews.get(jid); if (!_.isNil(view)) { return view.model.get('hidden') || _converse.windowState === 'hidden' || !u.isVisible(view.el); } return true; } return _converse.windowState === 'hidden'; } _converse.shouldNotifyOfMessage = function (message) { const forwarded = message.querySelector('forwarded'); if (!_.isNull(forwarded)) { return false; } else if (message.getAttribute('type') === 'groupchat') { return _converse.shouldNotifyOfGroupMessage(message); } else if (u.isHeadlineMessage(_converse, message)) { // We want to show notifications for headline messages. return _converse.isMessageToHiddenChat(message); } const is_me = Strophe.getBareJidFromJid( message.getAttribute('from')) === _converse.bare_jid; return !_converse.isOnlyChatStateNotification(message) && !is_me && (_converse.show_desktop_notifications === 'all' || _converse.isMessageToHiddenChat(message)); }; _converse.playSoundNotification = function () { /* Plays a sound to notify that a new message was recieved. */ // XXX Eventually this can be refactored to use Notification's sound // feature, but no browser currently supports it. // https://developer.mozilla.org/en-US/docs/Web/API/notification/sound if (_converse.play_sounds && !_.isUndefined(window.Audio)) { const audioOgg = new Audio(_converse.sounds_path+"msg_received.ogg"); const canPlayOgg = audioOgg.canPlayType('audio/ogg'); if (canPlayOgg === 'probably') { return audioOgg.play(); } const audioMp3 = new Audio(_converse.sounds_path+"msg_received.mp3"); const canPlayMp3 = audioMp3.canPlayType('audio/mp3'); if (canPlayMp3 === 'probably') { audioMp3.play(); } else if (canPlayOgg === 'maybe') { audioOgg.play(); } else if (canPlayMp3 === 'maybe') { audioMp3.play(); } } }; _converse.areDesktopNotificationsEnabled = function () { return _converse.supports_html5_notification && _converse.show_desktop_notifications && Notification.permission === "granted"; }; _converse.showMessageNotification = function (message) { /* Shows an HTML5 Notification to indicate that a new chat * message was received. */ if (!_converse.areDesktopNotificationsEnabled()) { return; } let title, roster_item; const full_from_jid = message.getAttribute('from'), from_jid = Strophe.getBareJidFromJid(full_from_jid); if (message.getAttribute('type') === 'headline') { if (!_.includes(from_jid, '@') || _converse.allow_non_roster_messaging) { title = __("Notification from %1$s", from_jid); } else { return; } } else if (!_.includes(from_jid, '@')) { // workaround for Prosody which doesn't give type "headline" title = __("Notification from %1$s", from_jid); } else if (message.getAttribute('type') === 'groupchat') { title = __("%1$s says", Strophe.getResourceFromJid(full_from_jid)); } else { if (_.isUndefined(_converse.roster)) { _converse.log( "Could not send notification, because roster is undefined", Strophe.LogLevel.ERROR); return; } roster_item = _converse.roster.get(from_jid); if (!_.isUndefined(roster_item)) { title = __("%1$s says", roster_item.getDisplayName()); } else { if (_converse.allow_non_roster_messaging) { title = __("%1$s says", from_jid); } else { return; } } } // TODO: we should suppress notifications if we cannot decrypt // the message... const body = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, message).length ? __('OMEMO Message received') : _.get(message.querySelector('body'), 'textContent'); if (!body) { return; } const n = new Notification(title, { 'body': body, 'lang': _converse.locale, 'icon': _converse.notification_icon, 'requireInteraction': !_converse.notification_delay }); if (_converse.notification_delay) { setTimeout(n.close.bind(n), _converse.notification_delay); } }; _converse.showChatStateNotification = function (contact) { /* Creates an HTML5 Notification to inform of a change in a * contact's chat state. */ if (_.includes(_converse.chatstate_notification_blacklist, contact.jid)) { // Don't notify if the user is being ignored. return; } const chat_state = contact.chat_status; let message = null; if (chat_state === 'offline') { message = __('has gone offline'); } else if (chat_state === 'away') { message = __('has gone away'); } else if ((chat_state === 'dnd')) { message = __('is busy'); } else if (chat_state === 'online') { message = __('has come online'); } if (message === null) { return; } const n = new Notification(contact.getDisplayName(), { body: message, lang: _converse.locale, icon: _converse.notification_icon }); setTimeout(n.close.bind(n), 5000); }; _converse.showContactRequestNotification = function (contact) { const n = new Notification(contact.getDisplayName(), { body: __('wants to be your contact'), lang: _converse.locale, icon: _converse.notification_icon }); setTimeout(n.close.bind(n), 5000); }; _converse.showFeedbackNotification = function (data) { if (data.klass === 'error' || data.klass === 'warn') { const n = new Notification(data.subject, { body: data.message, lang: _converse.locale, icon: _converse.notification_icon }); setTimeout(n.close.bind(n), 5000); } }; _converse.handleChatStateNotification = function (contact) { /* Event handler for on('contactPresenceChanged'). * Will show an HTML5 notification to indicate that the chat * status has changed. */ if (_converse.areDesktopNotificationsEnabled() && _converse.show_chatstate_notifications) { _converse.showChatStateNotification(contact); } }; _converse.handleMessageNotification = function (data) { /* Event handler for the on('message') event. Will call methods * to play sounds and show HTML5 notifications. */ const message = data.stanza; if (!_converse.shouldNotifyOfMessage(message)) { return false; } _converse.api.emit('messageNotification', message); _converse.playSoundNotification(); _converse.showMessageNotification(message); }; _converse.handleContactRequestNotification = function (contact) { if (_converse.areDesktopNotificationsEnabled(true)) { _converse.showContactRequestNotification(contact); } }; _converse.handleFeedback = function (data) { if (_converse.areDesktopNotificationsEnabled(true)) { _converse.showFeedbackNotification(data); } }; _converse.requestPermission = function () { if (_converse.supports_html5_notification && ! _.includes(['denied', 'granted'], Notification.permission)) { // Ask user to enable HTML5 notifications Notification.requestPermission(); } }; _converse.on('pluginsInitialized', function () { // We only register event handlers after all plugins are // registered, because other plugins might override some of our // handlers. _converse.on('contactRequest', _converse.handleContactRequestNotification); _converse.on('contactPresenceChanged', _converse.handleChatStateNotification); _converse.on('message', _converse.handleMessageNotification); _converse.on('feedback', _converse.handleFeedback); _converse.on('connected', _converse.requestPermission); }); } });