diff --git a/src/plugins/converse-rai.js b/src/plugins/converse-rai.js index c11090646e0a5ae692b4128828ddab8832e8d841..fb72df1e5b2adb9de11c01901941379d87e29b6f 100644 --- a/src/plugins/converse-rai.js +++ b/src/plugins/converse-rai.js @@ -1,195 +1,120 @@ -(function() { - let Strophe, $iq, $msg, $pres, $build, b64_sha1, _, Backbone, dayjs, _converse; - let interestingServers = new Set(); - let subscribedServers = new Set(); - - converse.plugins.add('converse-rai', { - 'dependencies': [], - - 'initialize': function() { - _converse = this._converse; - - Strophe = converse.env.Strophe; - $iq = converse.env.$iq; - $msg = converse.env.$msg; - $pres = converse.env.$pres; - $build = converse.env.$build; - b64_sha1 = converse.env.b64_sha1; - _ = converse.env._; - Backbone = converse.env.Backbone; - dayjs = converse.env.dayjs; - - _converse.api.settings.extend({ - rai_notification: true, - rai_notification_label: 'Room Activity Indicator', - }); - - _converse.api.listen.on('connected', function() { - setupRoomActivityIndicators(); - }); - - _converse.api.listen.on('chatRoomViewInitialized', function(view) { - const jid = view.model.get('jid'); - - if (view.model.get('num_unread') > 0 || view.model.get('num_unread_general') > 0) { - emitNotification(jid); - } - }); - - _converse.api.listen.on('raiRoomsUpdated', function(rooms) { - interestingServers = new Set(rooms.filter(room => room).map(Strophe.getDomainFromJid)); - if (_converse.api.connection.connected()) { - updateSubscriptions(); - } - }); - - _converse.api.listen.on('chatBoxScrolledDown', function(view) { - const jid = view.chatbox.get('jid'); - const id_attr = 'stanza_id ' + jid; - const messages = view.chatbox.messages; - - if (!getUnreadStatus(jid)) { - return; - } - - for (let i = messages.length - 1; i >= 0; i--) { - const message = messages.at(i); - - if (message.has(id_attr)) { - let id = message.get(id_attr); - if (id != sessionStorage.getItem('rai_displayed.' + jid)) { - sessionStorage.setItem('rai_displayed.' + jid, id); - setUnreadStatus(jid, false); - setTimeout(() => sendMarker(jid, id, 'displayed'), 0); +/** + * Add RAI support to MUCs. + * @see https://xmpp.org/extensions/inbox/room-activity-indicators.html + */ +converse.plugins.add('converse-rai', { + dependencies: [ + 'converse-chatboxes', + ], + overrides: { + ChatRoom: { + async initialize() { + await this.__super__.initialize.apply(this, arguments); + this.listenTo(this, 'change:hidden', this.onHiddenChange); + }, + + /** + * Handler that gets called when the 'hidden' flag is toggled. + * @private + * @method _converse.ChatRoom#onHiddenChange + */ + async onHiddenChange() { + const conn_status = this.session.get('connection_status'); + const { api } = this.__super__._converse; + + if (this.get('hidden') && conn_status === converse.ROOMSTATUS.ENTERED) { + if (api.settings.get('muc_subscribe_to_rai') && this.getOwnAffiliation() !== 'none') { + if (conn_status !== converse.ROOMSTATUS.DISCONNECTED) { + this.sendMarkerForLastMessage('received', true); + await this.leave(); + await this.close(); } - break; } + } else if (conn_status === converse.ROOMSTATUS.DISCONNECTED) { + await this.rejoin(); } - }); - - _converse.api.listen.on('chatBoxInsertedIntoDOM', function(view) { - const jid = view.model.get('jid'); + }, + }, + ChatBox: { + /** + * Finds the last eligible message and then sends a XEP-0333 chat marker for it. + * @param { ('received'|'displayed'|'acknowledged') } [type='displayed'] + * @param { Boolean } force - Whether a marker should be sent for the + * message, even if it didn't include a `markable` element. + */ + sendMarkerForLastMessage(type = 'displayed', force = false) { + const msgs = Array.from(this.messages.models); + msgs.reverse(); + const msg = msgs.find(m => m.get('sender') === 'them' && (force || m.get('is_markable'))); + msg && this.sendMarkerForMessage(msg, type, force); + }, + }, + }, + initialize() { + const { Strophe, _, $pres, u, sizzle, log } = converse.env; + const _converse = this._converse; + const { api } = _converse; + + // Register namespace + Strophe.addNamespace('RAI', 'urn:xmpp:rai:0'); + + // Register settings + api.settings.extend({ + muc_subscribe_to_rai: false, + }); - if (view.model.get('num_unread') > 0) { - emitNotification(jid); + Object.assign(api.rooms, { + /** + * Send an RAI stanza for the given jids. + * The presence stanza is sent for a whole muc domain. + * + * @param {string|Array} jids + * @returns {void} + */ + subscribe(jids) { + if (!api.settings.get('muc_subscribe_to_rai')) { + console.error('Can\'t subscribe to RAI, this feature is not enabled'); } - }); - - _converse.api.listen.on('message', function(data) { - var chatbox = data.chatbox; - var history = data.attrs.is_archived; - var sender = data.attrs.sender; - var body = data.attrs.body; - - if (!history && body && chatbox && sender !== 'me') { - const alert = chatbox.get('num_unread') > 0; - const notify = chatbox.get('num_unread_general') > 0; - - if (alert || notify) { - emitNotification(chatbox.get('jid'), alert); - } + if (typeof jids === 'string') { + jids = [jids]; } - }); - }, - }); + const muc_domains = jids.map(jid => Strophe.getDomainFromJid(jid)); - function subscribeServer(server_name) { - const id = Math.random().toString(36).substr(2, 9); - _converse.connection.send(converse.env.$pres({ - to: server_name, - id: id, - }).c('rai', { - 'xmlns': 'xmpp:prosody.im/protocol/rai', - })); - } - - function unsubscribeServer(server_name) { - _converse.connection.send(converse.env.$pres({ - to: server_name, type: 'unavailable', - }).c('rai', { - 'xmlns': 'xmpp:prosody.im/protocol/rai', - })); - } - - function updateSubscriptions() { - var new_servers = new Set([...interestingServers].filter(server => !subscribedServers.has(server))); - var obsolete_servers = new Set([...subscribedServers].filter(server => !interestingServers.has(server))); - for (let server of obsolete_servers) { - unsubscribeServer(server); - } - for (let server of new_servers) { - subscribeServer(server); - } - } + _.uniq(muc_domains).forEach(muc_domain => { + const rai = $pres({ to: muc_domain, id: u.getUniqueId() }).c('rai', { + 'xmlns': Strophe.NS.RAI, + }); + api.send(rai); + console.log('Sent RAI stanza for muc_domain', muc_domain, rai.toString()); + }); + }, + }); - function setupRoomActivityIndicators() { - updateSubscriptions(); - // If we already have unread notifications stored for this session, emit them now - for (var i = 0; i < sessionStorage.length; i++) { - if (sessionStorage.key(i).indexOf('rai_notify.') == 0) { - const jid = sessionStorage.key(i).substring(11); - emitNotification(jid); + /** + * Triggers an event if a jid has activity. + * + * @param message + * @returns {boolean} + */ + function mucActivityHandler(message) { + const rai = sizzle(`rai[xmlns="${Strophe.NS.RAI}"]`, message).pop(); + + if (rai) { + rai.querySelectorAll('activity').forEach(activity => { + const jid = activity.textContent; + log.debug(`Received activity for the room: ${jid}`); + api.trigger('chatRoomHasActivity', jid); + }); } - } - // Listen for incoming RAI from the server - _converse.connection.addHandler(function(message) { - const from_jid = message.attributes.from?.nodevalue; - const room_jid = from_jid?.split('/')[0]; - const room = ''; - let ignore = false; - for (let i = 0; i < _converse.chatboxes.models.length; i++) { - if (_converse.chatboxes.models[i].id === room_jid) { - room = _converse.chatboxes.models[i].id; - break; - } - } - if (room && from_jid && room_jid) { - if (from_jid === room_jid + '/' + room.get('nick')) { - ignore = true; - } - } - if (message && !ignore) - message.querySelectorAll('activity').forEach(function(activity) { - if (activity && activity.namespaceURI == 'xmpp:prosody.im/protocol/rai') { - const jid = activity.textContent; - setUnreadStatus(jid, true); - emitNotification(jid); - } - }); return true; - }, null, 'message'); - } - - function setUnreadStatus(jid, flag) { - if (flag) { - sessionStorage.setItem('rai_notify.' + jid, 'true'); - } else { - sessionStorage.removeItem('rai_notify.' + jid); } - } - - function getUnreadStatus(jid) { - return sessionStorage.getItem('rai_notify.' + jid) == 'true'; - } - function emitNotification(jid, alert) { - _converse.api.trigger('chatRoomActivityIndicators', jid); - } - - function sendMarker(to_jid, id, type) { - const stanza = converse.env.$msg({ - 'from': _converse.connection.jid, - 'id': Math.random().toString(36).substr(2, 9), - 'to': to_jid, - 'type': 'groupchat', - }).c(type, { - 'xmlns': converse.env.Strophe.NS.MARKERS, - 'id': id, + // Register our RAI handler + api.listen.on('connected', () => { + _converse.connection.addHandler(mucActivityHandler, null, 'message'); }); - - _converse.api.send(stanza); - } -})(); + }, +}); diff --git a/src/plugins/sib-subscribe-to-rai.js b/src/plugins/sib-subscribe-to-rai.js index 51dbb91174b448768f923173549ac9555694063b..e18baf76676125b85121b46258fc82adb062335f 100644 --- a/src/plugins/sib-subscribe-to-rai.js +++ b/src/plugins/sib-subscribe-to-rai.js @@ -51,15 +51,17 @@ converse.plugins.add('sib-subscribe-to-rai', { }, 250); }); + // @MattJ Here userRooms is an array of each jabberID the user is on. let userRooms = (await Promise.all([ getCircles, getProjects, ])).flat(); - // @MattJ Here userRooms is an array of each jabberID the user is on. - api.trigger('raiRoomsUpdated', userRooms); + api.listen.on('connected', () => { + api.rooms.subscribe(userRooms); + }); - api.listen.on('chatRoomActivityIndicators', jid => { + api.listen.on('chatRoomHasActivity', jid => { window.dispatchEvent(new CustomEvent('newMessage', { detail: { jid, diff --git a/src/solid-xmpp-chat.js b/src/solid-xmpp-chat.js index e6c792da921f140a734b19e4bb29759d77cfa955..8887e7ab131e173794aba07d10221da35c634299 100644 --- a/src/solid-xmpp-chat.js +++ b/src/solid-xmpp-chat.js @@ -205,6 +205,7 @@ export const SolidXMPPChat = { 'muc_nickname_from_jid': false, 'muc_fetch_members': true, 'muc_show_info_messages': [], + 'muc_subscribe_to_rai': true, 'play_sounds': false, 'root': this.element.shadowRoot, 'show_client_info': false,