Move converse-notifications plugin into a folder and split up
This commit is contained in:
parent
7f851208aa
commit
50dda3244e
@ -1,4 +1,4 @@
|
||||
/*global mock, converse, _ */
|
||||
/*global mock, converse */
|
||||
|
||||
const { Strophe } = converse.env;
|
||||
const $msg = converse.env.$msg;
|
||||
@ -15,9 +15,8 @@ describe("Notifications", function () {
|
||||
mock.initConverse(['rosterGroupsFetched'], {}, async (done, _converse) => {
|
||||
|
||||
await mock.waitForRoster(_converse, 'current');
|
||||
spyOn(_converse, 'showMessageNotification').and.callThrough();
|
||||
spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true);
|
||||
spyOn(_converse, 'isMessageToHiddenChat').and.returnValue(true);
|
||||
const stub = jasmine.createSpyObj('MyNotification', ['onclick', 'close']);
|
||||
spyOn(window, 'Notification').and.returnValue(stub);
|
||||
|
||||
const message = 'This message will show a desktop notification';
|
||||
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
|
||||
@ -30,8 +29,7 @@ describe("Notifications", function () {
|
||||
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
|
||||
await _converse.handleMessageStanza(msg); // This will emit 'message'
|
||||
await u.waitUntil(() => _converse.api.chatviews.get(sender_jid));
|
||||
expect(_converse.areDesktopNotificationsEnabled).toHaveBeenCalled();
|
||||
expect(_converse.showMessageNotification).toHaveBeenCalled();
|
||||
expect(window.Notification).toHaveBeenCalled();
|
||||
done();
|
||||
}));
|
||||
|
||||
@ -44,17 +42,9 @@ describe("Notifications", function () {
|
||||
if (!view.el.querySelectorAll('.chat-area').length) {
|
||||
view.renderChatArea();
|
||||
}
|
||||
let no_notification = false;
|
||||
if (typeof window.Notification === 'undefined') {
|
||||
no_notification = true;
|
||||
window.Notification = function () {
|
||||
return {
|
||||
'close': function () {}
|
||||
};
|
||||
};
|
||||
}
|
||||
spyOn(_converse, 'showMessageNotification').and.callThrough();
|
||||
spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true);
|
||||
|
||||
const stub = jasmine.createSpyObj('MyNotification', ['onclick', 'close']);
|
||||
spyOn(window, 'Notification').and.returnValue(stub);
|
||||
|
||||
// Test mention with setting false
|
||||
const nick = mock.chatroom_names[0];
|
||||
@ -66,8 +56,7 @@ describe("Notifications", function () {
|
||||
}).c('body').t(text).tree();
|
||||
_converse.connection._dataRecv(mock.createRequest(makeMsg('romeo: this will NOT show a notification')));
|
||||
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||
await u.waitUntil(() => _converse.areDesktopNotificationsEnabled.calls.count() === 0);
|
||||
expect(_converse.showMessageNotification).not.toHaveBeenCalled();
|
||||
expect(window.Notification).not.toHaveBeenCalled();
|
||||
|
||||
// Test reference
|
||||
const message_with_ref = $msg({
|
||||
@ -79,27 +68,21 @@ describe("Notifications", function () {
|
||||
.c('reference', {'xmlns':'urn:xmpp:reference:0', 'begin':'0', 'end':'5', 'type':'mention', 'uri':'xmpp:romeo@montague.lit'}).tree();
|
||||
_converse.connection._dataRecv(mock.createRequest(message_with_ref));
|
||||
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||
await u.waitUntil(() => _converse.areDesktopNotificationsEnabled.calls.count() === 1);
|
||||
expect(_converse.showMessageNotification).toHaveBeenCalled();
|
||||
expect(window.Notification.calls.count()).toBe(1);
|
||||
|
||||
// Test mention with setting true
|
||||
_converse.api.settings.set('notify_all_room_messages', true);
|
||||
_converse.connection._dataRecv(mock.createRequest(makeMsg('romeo: this will show a notification')));
|
||||
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||
await u.waitUntil(() => _converse.areDesktopNotificationsEnabled.calls.count() === 2);
|
||||
expect(_converse.showMessageNotification).toHaveBeenCalled();
|
||||
if (no_notification) {
|
||||
delete window.Notification;
|
||||
}
|
||||
expect(window.Notification.calls.count()).toBe(2);
|
||||
done();
|
||||
}));
|
||||
|
||||
it("is shown for headline messages",
|
||||
mock.initConverse(['rosterGroupsFetched'], {}, async (done, _converse) => {
|
||||
|
||||
spyOn(_converse, 'showMessageNotification').and.callThrough();
|
||||
spyOn(_converse, 'isMessageToHiddenChat').and.returnValue(true);
|
||||
spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true);
|
||||
const stub = jasmine.createSpyObj('MyNotification', ['onclick', 'close']);
|
||||
spyOn(window, 'Notification').and.returnValue(stub);
|
||||
const stanza = $msg({
|
||||
'type': 'headline',
|
||||
'from': 'notify.example.com',
|
||||
@ -116,14 +99,13 @@ describe("Notifications", function () {
|
||||
const view = _converse.chatboxviews.get('notify.example.com');
|
||||
await new Promise(resolve => view.model.messages.once('rendered', resolve));
|
||||
expect(_converse.chatboxviews.keys().includes('notify.example.com')).toBeTruthy();
|
||||
expect(_converse.showMessageNotification).toHaveBeenCalled();
|
||||
expect(window.Notification).toHaveBeenCalled();
|
||||
done();
|
||||
}));
|
||||
|
||||
it("is not shown for full JID headline messages if allow_non_roster_messaging is false", mock.initConverse((done, _converse) => {
|
||||
_converse.allow_non_roster_messaging = false;
|
||||
spyOn(_converse, 'showMessageNotification').and.callThrough();
|
||||
spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true);
|
||||
spyOn(window, 'Notification');
|
||||
const stanza = $msg({
|
||||
'type': 'headline',
|
||||
'from': 'someone@notify.example.com',
|
||||
@ -135,11 +117,8 @@ describe("Notifications", function () {
|
||||
.c('x', {'xmlns': 'jabber:x:oob'})
|
||||
.c('url').t('imap://romeo@example.com/INBOX;UIDVALIDITY=385759043/;UID=18');
|
||||
_converse.connection._dataRecv(mock.createRequest(stanza));
|
||||
expect(
|
||||
_.includes(_converse.chatboxviews.keys(),
|
||||
'someone@notify.example.com')
|
||||
).toBeFalsy();
|
||||
expect(_converse.showMessageNotification).not.toHaveBeenCalled();
|
||||
expect(_converse.chatboxviews.keys().includes('someone@notify.example.com')).toBeFalsy();
|
||||
expect(window.Notification).not.toHaveBeenCalled();
|
||||
done();
|
||||
}));
|
||||
|
||||
@ -148,12 +127,10 @@ describe("Notifications", function () {
|
||||
async (done, _converse) => {
|
||||
|
||||
await mock.waitForRoster(_converse, 'current', 3);
|
||||
spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true);
|
||||
spyOn(_converse, 'showChatStateNotification');
|
||||
spyOn(window, 'Notification');
|
||||
const jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
|
||||
_converse.roster.get(jid).presence.set('show', 'busy'); // This will emit 'contactStatusChanged'
|
||||
await u.waitUntil(() => _converse.areDesktopNotificationsEnabled.calls.count() === 1);
|
||||
expect(_converse.showChatStateNotification).toHaveBeenCalled();
|
||||
_converse.roster.get(jid).presence.set('show', 'dnd');
|
||||
expect(window.Notification).toHaveBeenCalled();
|
||||
done()
|
||||
}));
|
||||
});
|
||||
@ -161,11 +138,9 @@ describe("Notifications", function () {
|
||||
|
||||
describe("When a new contact request is received", function () {
|
||||
it("an HTML5 Notification is received", mock.initConverse((done, _converse) => {
|
||||
spyOn(_converse, 'areDesktopNotificationsEnabled').and.returnValue(true);
|
||||
spyOn(_converse, 'showContactRequestNotification');
|
||||
_converse.api.trigger('contactRequest', {'fullname': 'Peter Parker', 'jid': 'peter@parker.com'});
|
||||
expect(_converse.areDesktopNotificationsEnabled).toHaveBeenCalled();
|
||||
expect(_converse.showContactRequestNotification).toHaveBeenCalled();
|
||||
spyOn(window, 'Notification');
|
||||
_converse.api.trigger('contactRequest', {'getDisplayName': () => 'Peter Parker'});
|
||||
expect(window.Notification).toHaveBeenCalled();
|
||||
done();
|
||||
}));
|
||||
});
|
||||
@ -180,7 +155,10 @@ describe("Notifications", function () {
|
||||
mock.createContacts(_converse, 'current');
|
||||
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
|
||||
_converse.play_sounds = true;
|
||||
spyOn(_converse, 'playSoundNotification');
|
||||
|
||||
const stub = jasmine.createSpyObj('MyAudio', ['play', 'canPlayType']);
|
||||
spyOn(window, 'Audio').and.returnValue(stub);
|
||||
|
||||
const view = _converse.chatboxviews.get('lounge@montague.lit');
|
||||
if (!view.el.querySelectorAll('.chat-area').length) {
|
||||
view.renderChatArea();
|
||||
@ -194,8 +172,8 @@ describe("Notifications", function () {
|
||||
}).c('body').t(text);
|
||||
_converse.api.settings.set('notify_all_room_messages', true);
|
||||
await view.model.handleMessageStanza(message.nodeTree);
|
||||
await u.waitUntil(() => _converse.playSoundNotification.calls.count());
|
||||
expect(_converse.playSoundNotification).toHaveBeenCalled();
|
||||
await u.waitUntil(() => window.Audio.calls.count());
|
||||
expect(window.Audio).toHaveBeenCalled();
|
||||
|
||||
text = "This message won't play a sound";
|
||||
message = $msg({
|
||||
@ -205,7 +183,7 @@ describe("Notifications", function () {
|
||||
type: 'groupchat'
|
||||
}).c('body').t(text);
|
||||
await view.model.handleMessageStanza(message.nodeTree);
|
||||
expect(_converse.playSoundNotification, 1);
|
||||
expect(window.Audio, 1);
|
||||
_converse.play_sounds = false;
|
||||
|
||||
text = "This message won't play a sound because it is sent by romeo";
|
||||
@ -216,7 +194,7 @@ describe("Notifications", function () {
|
||||
type: 'groupchat'
|
||||
}).c('body').t(text);
|
||||
await view.model.handleMessageStanza(message.nodeTree);
|
||||
expect(_converse.playSoundNotification, 1);
|
||||
expect(window.Audio, 1);
|
||||
_converse.play_sounds = false;
|
||||
done();
|
||||
}));
|
||||
|
@ -21,7 +21,7 @@ import "./plugins/mam-views.js";
|
||||
import "./plugins/minimize/index.js"; // Allows chat boxes to be minimized
|
||||
import "./plugins/muc-views/index.js"; // Views related to MUC
|
||||
import "./plugins/headlines-view/index.js";
|
||||
import "./plugins/notifications.js";
|
||||
import "./plugins/notifications/index.js";
|
||||
import "./plugins/omemo.js";
|
||||
import "./plugins/profile/index.js";
|
||||
import "./plugins/push.js"; // XEP-0357 Push Notifications
|
||||
|
@ -1,337 +0,0 @@
|
||||
/**
|
||||
* @module converse-notification
|
||||
* @copyright 2020, the Converse.js contributors
|
||||
* @license Mozilla Public License (MPLv2)
|
||||
*/
|
||||
import Favico from 'favico.js-slevomat';
|
||||
import log from "@converse/headless/log";
|
||||
import { __ } from '../i18n';
|
||||
import { _converse, api, converse } from "@converse/headless/core";
|
||||
|
||||
const { Strophe } = converse.env;
|
||||
const u = converse.env.utils;
|
||||
|
||||
const supports_html5_notification = "Notification" in window;
|
||||
|
||||
converse.env.Favico = Favico;
|
||||
let favicon;
|
||||
|
||||
function updateUnreadFavicon () {
|
||||
if (api.settings.get('show_tab_notifications')) {
|
||||
favicon = favicon ?? new converse.env.Favico({type: 'circle', animation: 'pop'});
|
||||
const chats = _converse.chatboxes.models;
|
||||
const num_unread = chats.reduce((acc, chat) => (acc + (chat.get('num_unread') || 0)), 0);
|
||||
favicon.badge(num_unread);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
api.settings.extend({
|
||||
// ^ a list of JIDs to ignore concerning chat state notifications
|
||||
chatstate_notification_blacklist: [],
|
||||
notification_delay: 5000,
|
||||
notification_icon: 'logo/conversejs-filled.svg',
|
||||
notify_all_room_messages: false,
|
||||
notify_nicknames_without_references: false,
|
||||
play_sounds: true,
|
||||
show_chat_state_notifications: false,
|
||||
show_desktop_notifications: true,
|
||||
show_tab_notifications: true,
|
||||
sounds_path: api.settings.get("assets_path")+'/sounds/',
|
||||
});
|
||||
|
||||
/**
|
||||
* Is this a group message for which we should notify the user?
|
||||
* @private
|
||||
* @method _converse#shouldNotifyOfGroupMessage
|
||||
* @param { MUCMessageAttributes } attrs
|
||||
*/
|
||||
_converse.shouldNotifyOfGroupMessage = function (attrs) {
|
||||
if (!attrs?.body) {
|
||||
return false;
|
||||
}
|
||||
const jid = attrs.from;
|
||||
const muc_jid = attrs.from_muc;
|
||||
const notify_all = api.settings.get('notify_all_room_messages');
|
||||
const room = _converse.chatboxes.get(muc_jid);
|
||||
const resource = Strophe.getResourceFromJid(jid);
|
||||
const sender = resource && Strophe.unescapeNode(resource) || '';
|
||||
let is_mentioned = false;
|
||||
const nick = room.get('nick');
|
||||
|
||||
if (api.settings.get('notify_nicknames_without_references')) {
|
||||
is_mentioned = (new RegExp(`\\b${nick}\\b`)).test(attrs.body);
|
||||
}
|
||||
|
||||
const references_me = (r) => {
|
||||
const jid = r.uri.replace(/^xmpp:/, '');
|
||||
return jid == _converse.bare_jid || jid === `${muc_jid}/${nick}`;
|
||||
}
|
||||
const is_referenced = attrs.references.reduce((acc, r) => acc || references_me(r), false);
|
||||
const is_not_mine = sender !== nick;
|
||||
const should_notify_user = notify_all === true
|
||||
|| (Array.isArray(notify_all) && notify_all.includes(muc_jid))
|
||||
|| is_referenced
|
||||
|| is_mentioned;
|
||||
return is_not_mine && !!should_notify_user;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given parsed attributes for a message stanza, get the related
|
||||
* chatbox and check whether it's hidden.
|
||||
* @private
|
||||
* @method _converse#isMessageToHiddenChat
|
||||
* @param { MUCMessageAttributes } attrs
|
||||
*/
|
||||
_converse.isMessageToHiddenChat = function (attrs) {
|
||||
return _converse.chatboxes.get(attrs.from)?.isHidden() ?? false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @method _converse#shouldNotifyOfMessage
|
||||
* @param { MessageData|MUCMessageData } data
|
||||
*/
|
||||
_converse.shouldNotifyOfMessage = function (data) {
|
||||
const { attrs, stanza } = data;
|
||||
if (!attrs || stanza.querySelector('forwarded') !== null) {
|
||||
return false;
|
||||
}
|
||||
if (attrs['type'] === 'groupchat') {
|
||||
return _converse.shouldNotifyOfGroupMessage(attrs);
|
||||
} else if (attrs.is_headline) {
|
||||
// We want to show notifications for headline messages.
|
||||
return _converse.isMessageToHiddenChat(attrs);
|
||||
}
|
||||
const is_me = Strophe.getBareJidFromJid(attrs.from) === _converse.bare_jid;
|
||||
return !u.isOnlyChatStateNotification(stanza) &&
|
||||
!u.isOnlyMessageDeliveryReceipt(stanza) &&
|
||||
!is_me &&
|
||||
(api.settings.get('show_desktop_notifications') === 'all' || _converse.isMessageToHiddenChat(attrs));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Plays a notification sound
|
||||
* @private
|
||||
* @method _converse#playSoundNotification
|
||||
*/
|
||||
_converse.playSoundNotification = function () {
|
||||
if (api.settings.get('play_sounds') && window.Audio !== undefined) {
|
||||
const audioOgg = new Audio(api.settings.get('sounds_path')+"msg_received.ogg");
|
||||
const canPlayOgg = audioOgg.canPlayType('audio/ogg');
|
||||
if (canPlayOgg === 'probably') {
|
||||
return audioOgg.play();
|
||||
}
|
||||
const audioMp3 = new Audio(api.settings.get('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 supports_html5_notification &&
|
||||
api.settings.get('show_desktop_notifications') &&
|
||||
Notification.permission === "granted";
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows an HTML5 Notification with the passed in message
|
||||
* @private
|
||||
* @method _converse#showMessageNotification
|
||||
* @param { MessageData|MUCMessageData } data
|
||||
*/
|
||||
_converse.showMessageNotification = function (data) {
|
||||
const { attrs } = data;
|
||||
if (attrs.is_error) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_converse.areDesktopNotificationsEnabled()) {
|
||||
return;
|
||||
}
|
||||
let title, roster_item;
|
||||
const full_from_jid = attrs.from,
|
||||
from_jid = Strophe.getBareJidFromJid(full_from_jid);
|
||||
if (attrs.type === 'headline') {
|
||||
if (!from_jid.includes('@') || api.settings.get("allow_non_roster_messaging")) {
|
||||
title = __("Notification from %1$s", from_jid);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else if (!from_jid.includes('@')) {
|
||||
// workaround for Prosody which doesn't give type "headline"
|
||||
title = __("Notification from %1$s", from_jid);
|
||||
} else if (attrs.type === 'groupchat') {
|
||||
title = __("%1$s says", Strophe.getResourceFromJid(full_from_jid));
|
||||
} else {
|
||||
if (_converse.roster === undefined) {
|
||||
log.error("Could not send notification, because roster is undefined");
|
||||
return;
|
||||
}
|
||||
roster_item = _converse.roster.get(from_jid);
|
||||
if (roster_item !== undefined) {
|
||||
title = __("%1$s says", roster_item.getDisplayName());
|
||||
} else {
|
||||
if (api.settings.get("allow_non_roster_messaging")) {
|
||||
title = __("%1$s says", from_jid);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const body = attrs.is_encrypted ? __('Encrypted message received') : attrs.body;
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
const n = new Notification(title, {
|
||||
'body': body,
|
||||
'lang': _converse.locale,
|
||||
'icon': api.settings.get('notification_icon'),
|
||||
'requireInteraction': !_converse.notification_delay
|
||||
});
|
||||
if (api.settings.get('notification_delay')) {
|
||||
setTimeout(n.close.bind(n), api.settings.get('notification_delay'));
|
||||
}
|
||||
n.onclick = function (event) {
|
||||
event.preventDefault();
|
||||
window.focus();
|
||||
const chat = _converse.chatboxes.get(from_jid);
|
||||
chat.maybeShow(true);
|
||||
}
|
||||
n.onclick.bind(_converse);
|
||||
};
|
||||
|
||||
_converse.showChatStateNotification = function (contact) {
|
||||
/* Creates an HTML5 Notification to inform of a change in a
|
||||
* contact's chat state.
|
||||
*/
|
||||
if (_converse.chatstate_notification_blacklist.includes(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() && api.settings.get('show_chat_state_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.
|
||||
*/
|
||||
if (!_converse.shouldNotifyOfMessage(data)) {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Triggered when a notification (sound or HTML5 notification) for a new
|
||||
* message has will be made.
|
||||
* @event _converse#messageNotification
|
||||
* @type { MessageData|MUCMessageData}
|
||||
* @example _converse.api.listen.on('messageNotification', stanza => { ... });
|
||||
*/
|
||||
api.trigger('messageNotification', data);
|
||||
_converse.playSoundNotification();
|
||||
_converse.showMessageNotification(data);
|
||||
};
|
||||
|
||||
_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 (supports_html5_notification && !['denied', 'granted'].includes(Notification.permission)) {
|
||||
// Ask user to enable HTML5 notifications
|
||||
Notification.requestPermission();
|
||||
}
|
||||
};
|
||||
|
||||
/************************ BEGIN Event Handlers ************************/
|
||||
|
||||
api.listen.on('clearSession', () => (favicon = null)); // Needed for tests
|
||||
|
||||
api.waitUntil('chatBoxesInitialized').then(
|
||||
() => _converse.chatboxes.on('change:num_unread', updateUnreadFavicon));
|
||||
|
||||
api.listen.on('pluginsInitialized', function () {
|
||||
// We only register event handlers after all plugins are
|
||||
// registered, because other plugins might override some of our
|
||||
// handlers.
|
||||
api.listen.on('contactRequest', _converse.handleContactRequestNotification);
|
||||
api.listen.on('contactPresenceChanged', _converse.handleChatStateNotification);
|
||||
api.listen.on('message', _converse.handleMessageNotification);
|
||||
api.listen.on('feedback', _converse.handleFeedback);
|
||||
api.listen.on('connected', _converse.requestPermission);
|
||||
});
|
||||
}
|
||||
});
|
53
src/plugins/notifications/index.js
Normal file
53
src/plugins/notifications/index.js
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @module converse-notification
|
||||
* @copyright 2020, the Converse.js contributors
|
||||
* @license Mozilla Public License (MPLv2)
|
||||
*/
|
||||
import { _converse, api, converse } from '@converse/headless/core';
|
||||
import {
|
||||
clearFavicon,
|
||||
handleChatStateNotification,
|
||||
handleContactRequestNotification,
|
||||
handleFeedback,
|
||||
handleMessageNotification,
|
||||
requestPermission,
|
||||
updateUnreadFavicon
|
||||
} from './utils.js';
|
||||
|
||||
converse.plugins.add('converse-notification', {
|
||||
dependencies: ['converse-chatboxes'],
|
||||
|
||||
initialize () {
|
||||
api.settings.extend({
|
||||
// ^ a list of JIDs to ignore concerning chat state notifications
|
||||
chatstate_notification_blacklist: [],
|
||||
notification_delay: 5000,
|
||||
notification_icon: 'logo/conversejs-filled.svg',
|
||||
notify_all_room_messages: false,
|
||||
notify_nicknames_without_references: false,
|
||||
play_sounds: true,
|
||||
show_chat_state_notifications: false,
|
||||
show_desktop_notifications: true,
|
||||
show_tab_notifications: true,
|
||||
sounds_path: api.settings.get('assets_path') + '/sounds/'
|
||||
});
|
||||
|
||||
/************************ Event Handlers ************************/
|
||||
api.listen.on('clearSession', clearFavicon); // Needed for tests
|
||||
|
||||
api.waitUntil('chatBoxesInitialized').then(() =>
|
||||
_converse.chatboxes.on('change:num_unread', updateUnreadFavicon)
|
||||
);
|
||||
|
||||
api.listen.on('pluginsInitialized', function () {
|
||||
// We only register event handlers after all plugins are
|
||||
// registered, because other plugins might override some of our
|
||||
// handlers.
|
||||
api.listen.on('contactRequest', handleContactRequestNotification);
|
||||
api.listen.on('contactPresenceChanged', handleChatStateNotification);
|
||||
api.listen.on('message', handleMessageNotification);
|
||||
api.listen.on('feedback', handleFeedback);
|
||||
api.listen.on('connected', requestPermission);
|
||||
});
|
||||
}
|
||||
});
|
284
src/plugins/notifications/utils.js
Normal file
284
src/plugins/notifications/utils.js
Normal file
@ -0,0 +1,284 @@
|
||||
import Favico from 'favico.js-slevomat';
|
||||
import log from '@converse/headless/log';
|
||||
import { __ } from 'i18n';
|
||||
import { _converse, api, converse } from '@converse/headless/core';
|
||||
|
||||
const { Strophe, u } = converse.env;
|
||||
const supports_html5_notification = 'Notification' in window;
|
||||
|
||||
converse.env.Favico = Favico;
|
||||
|
||||
let favicon;
|
||||
|
||||
|
||||
export function isMessageToHiddenChat (attrs) {
|
||||
return _converse.isTestEnv() || (_converse.chatboxes.get(attrs.from)?.isHidden() ?? false);
|
||||
}
|
||||
|
||||
export function areDesktopNotificationsEnabled () {
|
||||
return _converse.isTestEnv() || (
|
||||
supports_html5_notification &&
|
||||
api.settings.get('show_desktop_notifications') &&
|
||||
Notification.permission === 'granted'
|
||||
);
|
||||
}
|
||||
|
||||
export function clearFavicon () {
|
||||
favicon = null;
|
||||
}
|
||||
|
||||
export function updateUnreadFavicon () {
|
||||
if (api.settings.get('show_tab_notifications')) {
|
||||
favicon = favicon ?? new converse.env.Favico({ type: 'circle', animation: 'pop' });
|
||||
const chats = _converse.chatboxes.models;
|
||||
const num_unread = chats.reduce((acc, chat) => acc + (chat.get('num_unread') || 0), 0);
|
||||
favicon.badge(num_unread);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this a group message for which we should notify the user?
|
||||
* @private
|
||||
* @param { MUCMessageAttributes } attrs
|
||||
*/
|
||||
export function shouldNotifyOfGroupMessage (attrs) {
|
||||
if (!attrs?.body) {
|
||||
return false;
|
||||
}
|
||||
const jid = attrs.from;
|
||||
const muc_jid = attrs.from_muc;
|
||||
const notify_all = api.settings.get('notify_all_room_messages');
|
||||
const room = _converse.chatboxes.get(muc_jid);
|
||||
const resource = Strophe.getResourceFromJid(jid);
|
||||
const sender = (resource && Strophe.unescapeNode(resource)) || '';
|
||||
let is_mentioned = false;
|
||||
const nick = room.get('nick');
|
||||
|
||||
if (api.settings.get('notify_nicknames_without_references')) {
|
||||
is_mentioned = new RegExp(`\\b${nick}\\b`).test(attrs.body);
|
||||
}
|
||||
|
||||
const references_me = r => {
|
||||
const jid = r.uri.replace(/^xmpp:/, '');
|
||||
return jid == _converse.bare_jid || jid === `${muc_jid}/${nick}`;
|
||||
};
|
||||
const is_referenced = attrs.references.reduce((acc, r) => acc || references_me(r), false);
|
||||
const is_not_mine = sender !== nick;
|
||||
const should_notify_user =
|
||||
notify_all === true ||
|
||||
(Array.isArray(notify_all) && notify_all.includes(muc_jid)) ||
|
||||
is_referenced ||
|
||||
is_mentioned;
|
||||
return is_not_mine && !!should_notify_user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @method shouldNotifyOfMessage
|
||||
* @param { MessageData|MUCMessageData } data
|
||||
*/
|
||||
function shouldNotifyOfMessage (data) {
|
||||
const { attrs, stanza } = data;
|
||||
if (!attrs || stanza.querySelector('forwarded') !== null) {
|
||||
return false;
|
||||
}
|
||||
if (attrs['type'] === 'groupchat') {
|
||||
return shouldNotifyOfGroupMessage(attrs);
|
||||
} else if (attrs.is_headline) {
|
||||
// We want to show notifications for headline messages.
|
||||
return isMessageToHiddenChat(attrs);
|
||||
}
|
||||
const is_me = Strophe.getBareJidFromJid(attrs.from) === _converse.bare_jid;
|
||||
return (
|
||||
!u.isOnlyChatStateNotification(stanza) &&
|
||||
!u.isOnlyMessageDeliveryReceipt(stanza) &&
|
||||
!is_me &&
|
||||
(api.settings.get('show_desktop_notifications') === 'all' || isMessageToHiddenChat(attrs))
|
||||
);
|
||||
}
|
||||
|
||||
export function showFeedbackNotification (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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an HTML5 Notification to inform of a change in a
|
||||
* contact's chat state.
|
||||
*/
|
||||
function showChatStateNotification (contact) {
|
||||
if (_converse.chatstate_notification_blacklist.includes(contact.jid)) {
|
||||
// Don't notify if the user is being ignored.
|
||||
return;
|
||||
}
|
||||
const chat_state = contact.presence.get('show');
|
||||
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(), 5000);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shows an HTML5 Notification with the passed in message
|
||||
* @private
|
||||
* @param { MessageData|MUCMessageData } data
|
||||
*/
|
||||
function showMessageNotification (data) {
|
||||
const { attrs } = data;
|
||||
if (attrs.is_error) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!areDesktopNotificationsEnabled()) {
|
||||
return;
|
||||
}
|
||||
let title, roster_item;
|
||||
const full_from_jid = attrs.from,
|
||||
from_jid = Strophe.getBareJidFromJid(full_from_jid);
|
||||
if (attrs.type === 'headline') {
|
||||
if (!from_jid.includes('@') || api.settings.get('allow_non_roster_messaging')) {
|
||||
title = __('Notification from %1$s', from_jid);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else if (!from_jid.includes('@')) {
|
||||
// workaround for Prosody which doesn't give type "headline"
|
||||
title = __('Notification from %1$s', from_jid);
|
||||
} else if (attrs.type === 'groupchat') {
|
||||
title = __('%1$s says', Strophe.getResourceFromJid(full_from_jid));
|
||||
} else {
|
||||
if (_converse.roster === undefined) {
|
||||
log.error('Could not send notification, because roster is undefined');
|
||||
return;
|
||||
}
|
||||
roster_item = _converse.roster.get(from_jid);
|
||||
if (roster_item !== undefined) {
|
||||
title = __('%1$s says', roster_item.getDisplayName());
|
||||
} else {
|
||||
if (api.settings.get('allow_non_roster_messaging')) {
|
||||
title = __('%1$s says', from_jid);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const body = attrs.is_encrypted ? __('Encrypted message received') : attrs.body;
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
const n = new Notification(title, {
|
||||
'body': body,
|
||||
'lang': _converse.locale,
|
||||
'icon': api.settings.get('notification_icon'),
|
||||
'requireInteraction': !_converse.notification_delay
|
||||
});
|
||||
if (api.settings.get('notification_delay')) {
|
||||
setTimeout(() => n.close(), api.settings.get('notification_delay'));
|
||||
}
|
||||
n.onclick = function (event) {
|
||||
event.preventDefault();
|
||||
window.focus();
|
||||
const chat = _converse.chatboxes.get(from_jid);
|
||||
chat.maybeShow(true);
|
||||
}
|
||||
}
|
||||
|
||||
function playSoundNotification () {
|
||||
if (api.settings.get('play_sounds') && window.Audio !== undefined) {
|
||||
const audioOgg = new Audio(api.settings.get('sounds_path') + 'msg_received.ogg');
|
||||
const canPlayOgg = audioOgg.canPlayType('audio/ogg');
|
||||
if (canPlayOgg === 'probably') {
|
||||
return audioOgg.play();
|
||||
}
|
||||
const audioMp3 = new Audio(api.settings.get('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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for the on('message') event. Will call methods
|
||||
* to play sounds and show HTML5 notifications.
|
||||
*/
|
||||
export function handleMessageNotification (data) {
|
||||
if (!shouldNotifyOfMessage(data)) {
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Triggered when a notification (sound or HTML5 notification) for a new
|
||||
* message has will be made.
|
||||
* @event _converse#messageNotification
|
||||
* @type { MessageData|MUCMessageData}
|
||||
* @example _converse.api.listen.on('messageNotification', stanza => { ... });
|
||||
*/
|
||||
api.trigger('messageNotification', data);
|
||||
playSoundNotification();
|
||||
showMessageNotification(data);
|
||||
}
|
||||
|
||||
export function handleFeedback (data) {
|
||||
if (areDesktopNotificationsEnabled(true)) {
|
||||
showFeedbackNotification(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for on('contactPresenceChanged').
|
||||
* Will show an HTML5 notification to indicate that the chat status has changed.
|
||||
*/
|
||||
export function handleChatStateNotification (contact) {
|
||||
if (areDesktopNotificationsEnabled() && api.settings.get('show_chat_state_notifications')) {
|
||||
showChatStateNotification(contact);
|
||||
}
|
||||
}
|
||||
|
||||
function showContactRequestNotification (contact) {
|
||||
const n = new Notification(contact.getDisplayName(), {
|
||||
body: __('wants to be your contact'),
|
||||
lang: _converse.locale,
|
||||
icon: _converse.notification_icon
|
||||
});
|
||||
setTimeout(() => n.close(), 5000);
|
||||
}
|
||||
|
||||
export function handleContactRequestNotification (contact) {
|
||||
if (areDesktopNotificationsEnabled(true)) {
|
||||
showContactRequestNotification(contact);
|
||||
}
|
||||
}
|
||||
|
||||
export function requestPermission () {
|
||||
if (supports_html5_notification && !['denied', 'granted'].includes(Notification.permission)) {
|
||||
// Ask user to enable HTML5 notifications
|
||||
Notification.requestPermission();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user