converse.js
Version:
Browser based XMPP chat client
236 lines (212 loc) • 8.22 kB
JavaScript
/**
* @typedef {import('./contacts').default} RosterContacts
*/
import _converse from '../../shared/_converse.js';
import api from '../../shared/api/index.js';
import converse from '../../shared/api/public.js';
import log from '@converse/log';
import { Model } from '@converse/skeletor';
import { RosterFilter } from '../../plugins/roster/filter.js';
import { PRIVATE_CHAT_TYPE } from '../../shared/constants';
import { initStorage } from '../../utils/storage.js';
import { shouldClearCache } from '../../utils/session.js';
const { $pres } = converse.env;
function initRoster() {
// Initialize the collections that represent the roster contacts and groups
const roster = new _converse.exports.RosterContacts();
Object.assign(_converse, { roster }); // XXX Deprecated
Object.assign(_converse.state, { roster });
const bare_jid = _converse.session.get('bare_jid');
let id = `converse.contacts-${bare_jid}`;
initStorage(roster, id);
const roster_filter = new RosterFilter();
Object.assign(_converse, { roster_filter }); // XXX Deprecated
Object.assign(_converse.state, { roster_filter });
roster_filter.id = `_converse.rosterfilter-${bare_jid}`;
initStorage(roster_filter, roster_filter.id);
roster_filter.fetch();
id = `converse-roster-model-${bare_jid}`;
roster.data = new Model();
roster.data.id = id;
initStorage(roster.data, id);
roster.data.fetch();
/**
* Triggered once the `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', roster);
}
/**
* Fetch all the roster groups, and then the roster contacts.
* Emit an event after fetching is done in each case.
* @param {boolean} 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) {
const connection = api.connection.get();
if (ignore_cache) {
connection.send_initial_presence = true;
}
const roster = /** @type {RosterContacts} */ (_converse.state.roster);
try {
await roster.fetchRosterContacts();
api.trigger('rosterContactsFetched', roster);
} catch (reason) {
log.error(reason);
} finally {
if (connection.send_initial_presence) {
api.user.presence.send();
_converse.state.profile.save({ presence: 'online' });
}
}
}
function updateUnreadCounter(chatbox) {
const roster = /** @type {RosterContacts} */ (_converse.state.roster);
const contact = roster?.get(chatbox.get('jid'));
contact?.save({ num_unread: chatbox.get('num_unread') });
}
let presence_ref;
function registerPresenceHandler() {
unregisterPresenceHandler();
const connection = api.connection.get();
presence_ref = connection.addHandler(
/** @param {Element} presence */
(presence) => {
const roster = /** @type {RosterContacts} */ (_converse.state.roster);
roster.presenceHandler(presence);
return true;
},
null,
'presence',
null
);
}
export function unregisterPresenceHandler() {
if (presence_ref) {
const connection = api.connection.get();
connection.deleteHandler(presence_ref);
presence_ref = null;
}
}
async function clearPresences() {
await _converse.state.presences?.clearStore();
}
/**
* Roster specific event handler for the clearSession event
*/
export async function onClearSession() {
await clearPresences();
if (shouldClearCache(_converse)) {
const roster = /** @type {RosterContacts} */ (_converse.state.roster);
if (roster) {
roster.data?.destroy();
await roster.clearStore();
delete _converse.state.roster;
Object.assign(_converse, { roster: undefined }); // XXX DEPRECATED
}
}
}
/**
* 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();
}
const roster = /** @type {RosterContacts} */ (_converse.state.roster);
roster.onConnected();
registerPresenceHandler();
populateRoster(!api.connection.get().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
!api.connection.get().hasResumed() && (await clearPresences());
} else {
const presences = new _converse.exports.Presences();
Object.assign(_converse, { presences });
Object.assign(_converse.state, { presences });
const bare_jid = _converse.session.get('bare_jid');
const id = `converse.presences-${bare_jid}`;
initStorage(presences, id, 'session');
// We might be continuing an existing session, so we fetch cached presence data.
await new Promise((r) => presences.fetch({ success: r, error: r }));
}
/**
* 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 {boolean}
* @example _converse.api.listen.on('presencesInitialized', (reconnecting) => { ... });
*/
api.trigger('presencesInitialized', reconnecting);
}
/**
* Roster specific event handler for the chatBoxesInitialized event
*/
export function onChatBoxesInitialized() {
const { chatboxes } = _converse.state;
chatboxes.on('change:num_unread', updateUnreadCounter);
chatboxes.on('add', (chatbox) => {
if (chatbox.get('type') === PRIVATE_CHAT_TYPE) {
chatbox.setModelContact(chatbox.get('jid'));
}
});
}
/**
* Roster specific handler for the rosterContactsFetched promise
*/
export function onRosterContactsFetched() {
const roster = /** @type {RosterContacts} */ (_converse.state.roster);
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.state.chatboxes.findWhere({ 'jid': contact.get('jid') });
chatbox?.setModelContact(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);
}
/**
* @param {import('./contact.js').default} contact
* @return {boolean}
*/
export function isUnsavedContact(contact) {
const bare_jid = _converse.session.get('bare_jid');
const is_self = bare_jid !== contact.get('jid');
return !is_self && !contact.get('subscription');
}