Skip to content
Snippets Groups Projects
converse-rai.js 6.39 KiB
Newer Older
/**
 * 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'));
ubermanu's avatar
ubermanu committed
    },
ubermanu's avatar
ubermanu committed
    const { Strophe, _, $pres, u, sizzle, log } = converse.env;
    const _converse = this._converse;
ubermanu's avatar
ubermanu committed
    const { api } = _converse;
    Strophe.addNamespace('RAI', 'urn:xmpp:rai:0');
ubermanu's avatar
ubermanu committed
    api.settings.extend({
ubermanu's avatar
ubermanu committed
      muc_subscribe_to_rai: false,
ubermanu's avatar
ubermanu committed
    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) {
ubermanu's avatar
ubermanu committed
        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', {
ubermanu's avatar
ubermanu committed
            'xmlns': Strophe.NS.RAI,

          await _converse.connection.send(rai);
          log.debug(`Sent RAI stanza for domain "${domain}"`);
        }
ubermanu's avatar
ubermanu committed
      },
    /**
     * 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);
      }
    });

ubermanu's avatar
ubermanu committed
    /**
     * Triggers an event if a jid has activity.
     *
     * @param message
     * @returns {boolean}
     */
    function mucActivityHandler(message) {
ubermanu's avatar
ubermanu committed
      const rai = sizzle(`rai[xmlns="${Strophe.NS.RAI}"]`, message).pop();
ubermanu's avatar
ubermanu committed
      if (rai) {
        rai.querySelectorAll('activity').forEach(activity => {
          const jid = activity.textContent;
ubermanu's avatar
ubermanu committed
          log.debug(`Received activity for the room: ${jid}`);
ubermanu's avatar
ubermanu committed
          api.trigger('chatRoomHasActivity', jid);
ubermanu's avatar
ubermanu committed
    // Register our RAI handler
ubermanu's avatar
ubermanu committed
    api.listen.on('connected', () => {
      _converse.connection.addHandler(mucActivityHandler, null, 'message');
ubermanu's avatar
ubermanu committed
  },