Converse converse.js

Source: headless/plugins/roster/utils.js

import log from "@converse/headless/log";
import { Model } from '@converse/skeletor/src/model.js';
import { RosterFilter } from '@converse/headless/plugins/roster/filter.js';
import { STATUS_WEIGHTS } from "../../shared/constants";
import { _converse, api, converse } from "@converse/headless/core";
import { initStorage } from '@converse/headless/utils/storage.js';
import { shouldClearCache } from '@converse/headless/utils/core.js';

const { $pres } = converse.env;


function initRoster () {
    // Initialize the collections that represent the roster contacts and groups
    const roster = _converse.roster = new _converse.RosterContacts();
    let id = `converse.contacts-${_converse.bare_jid}`;
    initStorage(roster, id);

    const filter = _converse.roster_filter = new RosterFilter();
    filter.id = `_converse.rosterfilter-${_converse.bare_jid}`;
    initStorage(filter, filter.id);
    filter.fetch();

    id = `converse-roster-model-${_converse.bare_jid}`;
    roster.data = new Model();
    roster.data.id = id;
    initStorage(roster.data, id);
    roster.data.fetch();
    /**
     * Triggered once the `_converse.RosterContacts`
     * been created, but not yet populated with data.
     * This event is useful when you want to create views for these collections.
     * @event _converse#chatBoxMaximized
     * @example _converse.api.listen.on('rosterInitialized', () => { ... });
     * @example _converse.api.waitUntil('rosterInitialized').then(() => { ... });
     */
    api.trigger('rosterInitialized');
}


/**
 * Fetch all the roster groups, and then the roster contacts.
 * Emit an event after fetching is done in each case.
 * @private
 * @param { Bool } ignore_cache - If set to to true, the local cache
 *      will be ignored it's guaranteed that the XMPP server
 *      will be queried for the roster.
 */
async function populateRoster (ignore_cache=false) {
    if (ignore_cache) {
        _converse.send_initial_presence = true;
    }
    try {
        await _converse.roster.fetchRosterContacts();
        api.trigger('rosterContactsFetched');
    } catch (reason) {
        log.error(reason);
    } finally {
        _converse.send_initial_presence && api.user.presence.send();
    }
}


function updateUnreadCounter (chatbox) {
    const contact = _converse.roster?.get(chatbox.get('jid'));
    contact?.save({'num_unread': chatbox.get('num_unread')});
}

function registerPresenceHandler () {
    unregisterPresenceHandler();
    _converse.presence_ref = _converse.connection.addHandler(presence => {
            _converse.roster.presenceHandler(presence);
            return true;
        }, null, 'presence', null);
}

export function unregisterPresenceHandler () {
    if (_converse.presence_ref !== undefined) {
        _converse.connection.deleteHandler(_converse.presence_ref);
        delete _converse.presence_ref;
    }
}

async function clearPresences () {
    await _converse.presences?.clearStore();
}


/**
 * Roster specific event handler for the clearSession event
 */
export async function onClearSession () {
    await clearPresences();
    if (shouldClearCache()) {
        if (_converse.rostergroups) {
            await _converse.rostergroups.clearStore();
            delete _converse.rostergroups;
        }
        if (_converse.roster) {
            _converse.roster.data?.destroy();
            await _converse.roster.clearStore();
            delete _converse.roster;
        }
    }
}


/**
 * Roster specific event handler for the presencesInitialized event
 * @param { Boolean } reconnecting
 */
export function onPresencesInitialized (reconnecting) {
    if (reconnecting) {
        /**
         * Similar to `rosterInitialized`, but instead pertaining to reconnection.
         * This event indicates that the roster and its groups are now again
         * available after Converse.js has reconnected.
         * @event _converse#rosterReadyAfterReconnection
         * @example _converse.api.listen.on('rosterReadyAfterReconnection', () => { ... });
         */
        api.trigger('rosterReadyAfterReconnection');
    } else {
        initRoster();
    }
    _converse.roster.onConnected();
    registerPresenceHandler();
    populateRoster(!_converse.connection.restored);
}


/**
 * Roster specific event handler for the statusInitialized event
 * @param { Boolean } reconnecting
 */
export async function onStatusInitialized (reconnecting) {
     if (reconnecting) {
         // When reconnecting and not resuming a previous session,
         // we clear all cached presence data, since it might be stale
         // and we'll receive new presence updates
         !_converse.connection.hasResumed() && (await clearPresences());
     } else {
         _converse.presences = new _converse.Presences();
         const id = `converse.presences-${_converse.bare_jid}`;
         initStorage(_converse.presences, id, 'session');
         // We might be continuing an existing session, so we fetch
         // cached presence data.
         _converse.presences.fetch();
     }
     /**
      * Triggered once the _converse.Presences collection has been
      * initialized and its cached data fetched.
      * Returns a boolean indicating whether this event has fired due to
      * Converse having reconnected.
      * @event _converse#presencesInitialized
      * @type { bool }
      * @example _converse.api.listen.on('presencesInitialized', reconnecting => { ... });
      */
     api.trigger('presencesInitialized', reconnecting);
}


/**
 * Roster specific event handler for the chatBoxesInitialized event
 */
export function onChatBoxesInitialized () {
    _converse.chatboxes.on('change:num_unread', updateUnreadCounter);

    _converse.chatboxes.on('add', chatbox => {
        if (chatbox.get('type') === _converse.PRIVATE_CHAT_TYPE) {
            chatbox.setRosterContact(chatbox.get('jid'));
        }
    });
}


/**
 * Roster specific handler for the rosterContactsFetched promise
 */
export function onRosterContactsFetched () {
    _converse.roster.on('add', contact => {
        // When a new contact is added, check if we already have a
        // chatbox open for it, and if so attach it to the chatbox.
        const chatbox = _converse.chatboxes.findWhere({ 'jid': contact.get('jid') });
        chatbox?.setRosterContact(contact.get('jid'));
    });
}

/**
 * Reject or cancel another user's subscription to our presence updates.
 * @function rejectPresenceSubscription
 * @param { String } jid - The Jabber ID of the user whose subscription is being canceled
 * @param { String } message - An optional message to the user
 */
export function rejectPresenceSubscription (jid, message) {
    const pres = $pres({to: jid, type: "unsubscribed"});
    if (message && message !== "") { pres.c("status").t(message); }
    api.send(pres);
}

export function contactsComparator (contact1, contact2) {
    const status1 = contact1.presence.get('show') || 'offline';
    const status2 = contact2.presence.get('show') || 'offline';
    if (STATUS_WEIGHTS[status1] === STATUS_WEIGHTS[status2]) {
        const name1 = (contact1.getDisplayName()).toLowerCase();
        const name2 = (contact2.getDisplayName()).toLowerCase();
        return name1 < name2 ? -1 : (name1 > name2? 1 : 0);
    } else  {
        return STATUS_WEIGHTS[status1] < STATUS_WEIGHTS[status2] ? -1 : 1;
    }
}

export function groupsComparator (a, b) {
    const HEADER_WEIGHTS = {};
    HEADER_WEIGHTS[_converse.HEADER_UNREAD] = 0;
    HEADER_WEIGHTS[_converse.HEADER_REQUESTING_CONTACTS] = 1;
    HEADER_WEIGHTS[_converse.HEADER_CURRENT_CONTACTS]    = 2;
    HEADER_WEIGHTS[_converse.HEADER_UNGROUPED]           = 3;
    HEADER_WEIGHTS[_converse.HEADER_PENDING_CONTACTS]    = 4;

    const WEIGHTS =  HEADER_WEIGHTS;
    const special_groups = Object.keys(HEADER_WEIGHTS);
    const a_is_special = special_groups.includes(a);
    const b_is_special = special_groups.includes(b);
    if (!a_is_special && !b_is_special ) {
        return a.toLowerCase() < b.toLowerCase() ? -1 : (a.toLowerCase() > b.toLowerCase() ? 1 : 0);
    } else if (a_is_special && b_is_special) {
        return WEIGHTS[a] < WEIGHTS[b] ? -1 : (WEIGHTS[a] > WEIGHTS[b] ? 1 : 0);
    } else if (!a_is_special && b_is_special) {
        const a_header = _converse.HEADER_CURRENT_CONTACTS;
        return WEIGHTS[a_header] < WEIGHTS[b] ? -1 : (WEIGHTS[a_header] > WEIGHTS[b] ? 1 : 0);
    } else if (a_is_special && !b_is_special) {
        const b_header = _converse.HEADER_CURRENT_CONTACTS;
        return WEIGHTS[a] < WEIGHTS[b_header] ? -1 : (WEIGHTS[a] > WEIGHTS[b_header] ? 1 : 0);
    }
}

export function getGroupsAutoCompleteList () {
    const { roster } = _converse;
    const groups = roster.reduce((groups, contact) => groups.concat(contact.get('groups')), []);
    return [...new Set(groups.filter(i => i))];
}