/** * 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 && api.settings.get('muc_subscribe_to_rai') // && this.getOwnAffiliation() !== 'none' ) { this.sendMarkerForLastMessage('received', true); await this.leave(); await this.close(); } else if (conn_status === converse.ROOMSTATUS.DISCONNECTED) { await this.rejoin(); } }, /** * Subscribe to RAI if connected to the room, but it's hidden. * @private * @method _converse.ChatRoom#onConnectionStatusChanged */ async onConnectionStatusChanged() { const conn_status = this.session.get('connection_status'); const { api } = this.__super__._converse; if (this.get('hidden') && conn_status === converse.ROOMSTATUS.ENTERED && api.settings.get('muc_subscribe_to_rai') // && this.getOwnAffiliation() !== 'none' ) { await this.leave(); await this.close(); this.enableRAI(); } else { await this.__super__.onConnectionStatusChanged.apply(this, arguments); } }, /** * Ensures that the user is subscribed to XEP-0437 Room Activity Indicators * if `muc_subscribe_to_rai` is set to `true`. * Only affiliated users can subscribe to RAI, but this method doesn't * check whether the current user is affiliated because it's intended to be * called after the MUC has been left and we don't have that information * anymore. * @private * @method _converse.ChatRoom#enableRAI */ enableRAI() { const { api } = this.__super__._converse; if (api.settings.get('muc_subscribe_to_rai')) { api.rooms.subscribe(this.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); }, /** * Given the passed in message object, send a XEP-0333 chat marker. * @param { _converse.Message } msg * @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. */ sendMarkerForMessage(msg, type = 'displayed', force = false) { const _converse = this.__super__._converse; const { Strophe } = converse.env; // Markers should be sent for message with a stanza_id const is_groupchat = this.get('type') === _converse.CHATROOMS_TYPE; const by_jid = is_groupchat ? this.get('jid') : _converse.bare_jid; const stanza_id = msg.get('stanza_id '.concat(by_jid)); if (msg && (msg?.get('is_markable') || force) && stanza_id) { const from_jid = Strophe.getBareJidFromJid(msg.get('from')); this.sendMarker(from_jid, stanza_id, type, msg.get('type')); } }, }, }, initialize() { const { Strophe, _, $pres, u, sizzle, log } = converse.env; const _converse = this._converse; const { api } = _converse; Strophe.addNamespace('RAI', 'urn:xmpp:rai:0'); api.settings.extend({ muc_subscribe_to_rai: false, }); 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} */ async subscribe(jids) { if (!api.settings.get('muc_subscribe_to_rai')) { log.error(`You must enable the 'muc_subscribe_to_rai' option before subscribing to a MUC.`); return; } if (typeof jids === 'string') { jids = [jids]; } const muc_domains = _.uniq(jids.map(jid => Strophe.getDomainFromJid(jid))); for (let domain of muc_domains) { const rai = $pres({ to: domain, id: u.getUniqueId() }).c('rai', { 'xmlns': Strophe.NS.RAI, }); await _converse.connection.send(rai); log.debug(`Sent RAI stanza for domain "${domain}"`); } }, }); /** * Send a displayed marker once the chatroom is scrolled down. * FIXME: This is sending two requests (for the same message) */ api.listen.on('chatBoxScrolledDown', data => { if (data.chatbox.get('type') === _converse.CHATROOMS_TYPE && api.settings.get('muc_subscribe_to_rai')) { data.chatbox.sendMarkerForLastMessage('displayed', true); } }); /** * 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); }); } return true; } // Register our RAI handler api.listen.on('connected', () => { _converse.connection.addHandler(mucActivityHandler, null, 'message'); }); }, });