Refactor _converse.XMPPStatus out into headless/converse-status.js

Also move some other methods out of `converse-core` into the plugins
that use them.
This commit is contained in:
JC Brand 2019-10-15 12:55:11 +02:00
parent 1a7f58b578
commit b6d5077d04
11 changed files with 449 additions and 398 deletions

View File

@ -10,6 +10,7 @@
- Initial support for sending custom emojis. Currently only between Converse
instances. Still working out a wire protocol for compatibility with other clients.
To add custom emojis, edit the `emojis.json` file.
- Refactor some presence and status handling code from `converse-core` into `@converse/headless/converse-status`.
### Breaking changes
@ -26,6 +27,7 @@
* `_converse.api.rooms.create`
- The `show_only_online_users` setting has been removed.
- The order of certain events have now changed: `statusInitialized` is now triggered after `initialized` and `connected` and `reconnected`.
## 5.0.4 (2019-10-08)
- New config option [allow_message_corrections](https://conversejs.org/docs/html/configuration.html#allow-message-corrections)

View File

@ -1138,15 +1138,14 @@
describe("A Message Counter", function () {
it("is incremented when the message is received and the window is not focused",
mock.initConverse(
['rosterGroupsFetched'], {},
async function (done, _converse) {
mock.initConverse(
['rosterGroupsFetched'], {},
async function (done, _converse) {
await test_utils.waitForRoster(_converse, 'current');
test_utils.openControlBox();
expect(_converse.msg_counter).toBe(0);
expect(document.title).toBe('Converse Tests');
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const view = await test_utils.openChatBoxFor(_converse, sender_jid)
@ -1170,7 +1169,7 @@
await new Promise(resolve => view.once('messageInserted', resolve));
expect(_converse.incrementMsgCounter).toHaveBeenCalled();
expect(_converse.clearMsgCounter).not.toHaveBeenCalled();
expect(_converse.msg_counter).toBe(1);
expect(document.title).toBe('Messages (1) Converse Tests');
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
_converse.windowSate = previous_state;
done();
@ -1199,7 +1198,7 @@
await test_utils.waitForRoster(_converse, 'current');
test_utils.openControlBox();
expect(_converse.msg_counter).toBe(0);
expect(document.title).toBe('Converse Tests');
spyOn(_converse, 'incrementMsgCounter').and.callThrough();
_converse.saveWindowState(null, 'focus');
const message = 'This message will not increment the message counter';
@ -1213,7 +1212,7 @@
.c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree();
await _converse.chatboxes.onMessage(msg);
expect(_converse.incrementMsgCounter).not.toHaveBeenCalled();
expect(_converse.msg_counter).toBe(0);
expect(document.title).toBe('Converse Tests');
done();
}));
@ -1224,7 +1223,7 @@
await test_utils.waitForRoster(_converse, 'current');
// initial state
expect(_converse.msg_counter).toBe(0);
expect(document.title).toBe('Converse Tests');
const message = 'This message will always increment the message counter from zero',
sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
msgFactory = function () {
@ -1244,12 +1243,12 @@
_converse.chatboxes.onMessage(msgFactory());
await u.waitUntil(() => _converse.api.chats.get().length === 2)
let view = _converse.chatboxviews.get(sender_jid);
expect(_converse.msg_counter).toBe(1);
expect(document.title).toBe('Messages (1) Converse Tests');
// come back to converse-chat page
_converse.saveWindowState(null, 'focus');
expect(u.isVisible(view.el)).toBeTruthy();
expect(_converse.msg_counter).toBe(0);
expect(document.title).toBe('Converse Tests');
// close chatbox and leave converse-chat page again
view.close();
@ -1260,7 +1259,7 @@
await u.waitUntil(() => _converse.api.chats.get().length === 2)
view = _converse.chatboxviews.get(sender_jid);
expect(u.isVisible(view.el)).toBeTruthy();
expect(_converse.msg_counter).toBe(1);
expect(document.title).toBe('Messages (1) Converse Tests');
done();
}));
});

View File

@ -50,16 +50,17 @@
await test_utils.waitForRoster(_converse, 'current', 1);
iq = IQ_stanzas.pop();
IQ_stanzas.pop();
const disco_iq = IQ_stanzas.pop();
expect(Strophe.serialize(disco_iq)).toBe(
`<iq from="romeo@montague.lit" id="${disco_iq.getAttribute('id')}" to="romeo@montague.lit" type="get" xmlns="jabber:client">`+
`<pubsub xmlns="http://jabber.org/protocol/pubsub"><items node="eu.siacs.conversations.axolotl.devicelist"/></pubsub></iq>`);
iq = IQ_stanzas.pop();
expect(Strophe.serialize(iq)).toBe(
`<iq from="romeo@montague.lit/orchard" id="${iq.getAttribute('id')}" to="montague.lit" type="get" xmlns="jabber:client">`+
`<query xmlns="http://jabber.org/protocol/disco#info"/></iq>`);
const disco_iq = IQ_stanzas.pop();
expect(Strophe.serialize(disco_iq)).toBe(
`<iq from="romeo@montague.lit" id="${disco_iq.getAttribute('id')}" to="romeo@montague.lit" type="get" xmlns="jabber:client">`+
`<pubsub xmlns="http://jabber.org/protocol/pubsub"><items node="eu.siacs.conversations.axolotl.devicelist"/></pubsub></iq>`);
expect(sent_stanzas.filter(s => (s.nodeName === 'r')).length).toBe(2);
expect(_converse.session.get('unacked_stanzas').length).toBe(5);

View File

@ -7,6 +7,7 @@
/**
* @module converse-profile
*/
import "@converse/headless/converse-status";
import "@converse/headless/converse-vcard";
import "converse-modal";
import "formdata-polyfill";
@ -23,7 +24,7 @@ const u = converse.env.utils;
converse.plugins.add('converse-profile', {
dependencies: ["converse-modal", "converse-vcard", "converse-chatboxviews"],
dependencies: ["converse-status", "converse-modal", "converse-vcard", "converse-chatboxviews"],
initialize () {
/* The initialize function gets called as soon as the plugin is

View File

@ -49,6 +49,33 @@ converse.plugins.add('converse-chatboxes', {
'privateChatsAutoJoined'
]);
let msg_counter = 0;
_converse.incrementMsgCounter = function () {
msg_counter += 1;
const title = document.title;
if (!title) {
return;
}
if (title.search(/^Messages \(\d+\) /) === -1) {
document.title = `Messages (${msg_counter}) ${title}`;
} else {
document.title = title.replace(/^Messages \(\d+\) /, `Messages (${msg_counter})`);
}
};
_converse.clearMsgCounter = function () {
msg_counter = 0;
const title = document.title;
if (!title) {
return;
}
if (title.search(/^Messages \(\d+\) /) !== -1) {
document.title = title.replace(/^Messages \(\d+\) /, "");
}
};
function openChat (jid) {
if (!utils.isValidJID(jid)) {
return _converse.log(
@ -1405,6 +1432,7 @@ converse.plugins.add('converse-chatboxes', {
_converse.api.listen.on('presencesInitialized', (reconnecting) => _converse.chatboxes.onConnected(reconnecting));
_converse.api.listen.on('reconnected', () => _converse.chatboxes.forEach(m => m.onReconnection()));
_converse.api.listen.on('windowStateChanged', d => (d.state === 'visible') && _converse.clearMsgCounter());
/************************ END Event Handlers ************************/

View File

@ -86,6 +86,7 @@ const CORE_PLUGINS = [
'converse-roster',
'converse-rsm',
'converse-smacks',
'converse-status',
'converse-vcard'
];
@ -560,11 +561,6 @@ function clearSession () {
_converse.session.browserStorage._clear();
delete _converse.session;
}
if (_converse.shouldClearCache() && _converse.xmppstatus) {
_converse.xmppstatus.destroy();
_converse.xmppstatus.browserStorage._clear();
delete _converse.xmppstatus;
}
/**
* Triggered once the user session has been cleared,
* for example when the user has logged out or when Converse has
@ -739,6 +735,33 @@ _converse.setUserJID = async function (jid) {
}
function enableCarbons () {
/* Ask the XMPP server to enable Message Carbons
* See XEP-0280 https://xmpp.org/extensions/xep-0280.html#enabling
*/
if (!_converse.message_carbons || !_converse.session || _converse.session.get('carbons_enabled')) {
return;
}
const carbons_iq = new Strophe.Builder('iq', {
'from': _converse.connection.jid,
'id': 'enablecarbons',
'type': 'set'
})
.c('enable', {xmlns: Strophe.NS.CARBONS});
_converse.connection.addHandler((iq) => {
if (iq.querySelectorAll('error').length > 0) {
_converse.log(
'An error occurred while trying to enable message carbons.',
Strophe.LogLevel.WARN);
} else {
_converse.session.save({'carbons_enabled': true});
_converse.log('Message carbons have been enabled.');
}
}, null, "iq", null, "enablecarbons");
_converse.connection.send(carbons_iq);
}
async function onConnected (reconnecting) {
/* Called as soon as a new connection has been established, either
* by logging in or by attaching to an existing BOSH session.
@ -751,9 +774,33 @@ async function onConnected (reconnecting) {
* user's JID resource for this session.
* @event _converse#afterResourceBinding
*/
await _converse.api.trigger('afterResourceBinding', {'synchronous': true});
_converse.enableCarbons();
_converse.initStatus(reconnecting)
await _converse.api.trigger('afterResourceBinding', reconnecting, {'synchronous': true});
enableCarbons();
if (reconnecting) {
/**
* After the connection has dropped and converse.js has reconnected.
* Any Strophe stanza handlers (as registered via `converse.listen.stanza`) will
* have to be registered anew.
* @event _converse#reconnected
* @example _converse.api.listen.on('reconnected', () => { ... });
*/
_converse.api.trigger('reconnected');
} else {
/**
* Triggered once converse.js has been initialized.
* See also {@link _converse#event:pluginsInitialized}.
* @event _converse#initialized
*/
_converse.api.trigger('initialized');
/**
* Triggered after the connection has been established and Converse
* has got all its ducks in a row.
* @event _converse#initialized
*/
_converse.api.trigger('connected');
}
}
@ -895,7 +942,6 @@ _converse.initialize = async function (settings, callback) {
cleanup();
settings = settings !== undefined ? settings : {};
const init_promise = u.getResolveablePromise();
PROMISES.forEach(addPromise);
if ('onpagehide' in window) {
@ -955,7 +1001,6 @@ _converse.initialize = async function (settings, callback) {
* https://github.com/jcbrand/converse.js/issues/521
*/
this.send_initial_presence = true;
this.msg_counter = 0;
this.user_settings = settings; // Save the user settings so that they can be used by plugins
// Module-level functions
@ -963,107 +1008,6 @@ _converse.initialize = async function (settings, callback) {
this.generateResource = () => `/converse.js-${Math.floor(Math.random()*139749528).toString()}`;
/**
* Send out a Client State Indication (XEP-0352)
* @private
* @method sendCSI
* @memberOf _converse
* @param { String } stat - The user's chat status
*/
this.sendCSI = function (stat) {
_converse.api.send($build(stat, {xmlns: Strophe.NS.CSI}));
_converse.inactive = (stat === _converse.INACTIVE) ? true : false;
};
this.onUserActivity = function () {
/* Resets counters and flags relating to CSI and auto_away/auto_xa */
if (_converse.idle_seconds > 0) {
_converse.idle_seconds = 0;
}
if (!_.get(_converse.connection, 'authenticated')) {
// We can't send out any stanzas when there's no authenticated connection.
// This can happen when the connection reconnects.
return;
}
if (_converse.inactive) {
_converse.sendCSI(_converse.ACTIVE);
}
if (_converse.idle) {
_converse.idle = false;
_converse.xmppstatus.sendPresence();
}
if (_converse.auto_changed_status === true) {
_converse.auto_changed_status = false;
// XXX: we should really remember the original state here, and
// then set it back to that...
_converse.xmppstatus.set('status', _converse.default_state);
}
};
this.onEverySecond = function () {
/* An interval handler running every second.
* Used for CSI and the auto_away and auto_xa features.
*/
if (!_.get(_converse.connection, 'authenticated')) {
// We can't send out any stanzas when there's no authenticated connection.
// This can happen when the connection reconnects.
return;
}
const stat = _converse.xmppstatus.get('status');
_converse.idle_seconds++;
if (_converse.csi_waiting_time > 0 &&
_converse.idle_seconds > _converse.csi_waiting_time &&
!_converse.inactive) {
_converse.sendCSI(_converse.INACTIVE);
}
if (_converse.idle_presence_timeout > 0 &&
_converse.idle_seconds > _converse.idle_presence_timeout &&
!_converse.idle) {
_converse.idle = true;
_converse.xmppstatus.sendPresence();
}
if (_converse.auto_away > 0 &&
_converse.idle_seconds > _converse.auto_away &&
stat !== 'away' && stat !== 'xa' && stat !== 'dnd') {
_converse.auto_changed_status = true;
_converse.xmppstatus.set('status', 'away');
} else if (_converse.auto_xa > 0 &&
_converse.idle_seconds > _converse.auto_xa &&
stat !== 'xa' && stat !== 'dnd') {
_converse.auto_changed_status = true;
_converse.xmppstatus.set('status', 'xa');
}
};
this.registerIntervalHandler = function () {
/* Set an interval of one second and register a handler for it.
* Required for the auto_away, auto_xa and csi_waiting_time features.
*/
if (
_converse.auto_away < 1 &&
_converse.auto_xa < 1 &&
_converse.csi_waiting_time < 1 &&
_converse.idle_presence_timeout < 1
) {
// Waiting time of less then one second means features aren't used.
return;
}
_converse.idle_seconds = 0;
_converse.auto_changed_status = false; // Was the user's status changed by Converse?
window.addEventListener('click', _converse.onUserActivity);
window.addEventListener('focus', _converse.onUserActivity);
window.addEventListener('keypress', _converse.onUserActivity);
window.addEventListener('mousemove', _converse.onUserActivity);
const options = {'once': true, 'passive': true};
window.addEventListener(_converse.unloadevent, _converse.onUserActivity, options);
window.addEventListener(_converse.unloadevent, () => {
if (_converse.session) {
_converse.session.save('active', false);
}
});
_converse.everySecondTrigger = window.setInterval(_converse.onEverySecond, 1000);
};
this.setConnectionStatus = function (connection_status, message) {
_converse.connfeedback.set({
'connection_status': connection_status,
@ -1071,21 +1015,6 @@ _converse.initialize = async function (settings, callback) {
});
};
/**
* Reject or cancel another user's subscription to our presence updates.
* @method rejectPresenceSubscription
* @private
* @memberOf _converse
* @param { String } jid - The Jabber ID of the user whose subscription is being canceled
* @param { String } message - An optional message to the user
*/
this.rejectPresenceSubscription = function (jid, message) {
const pres = $pres({to: jid, type: "unsubscribed"});
if (message && message !== "") { pres.c("status").t(message); }
_converse.api.send(pres);
};
/**
* Gets called once strophe's status reaches Strophe.Status.DISCONNECTED.
* Will either start a teardown process for converse.js or attempt
@ -1134,11 +1063,15 @@ _converse.initialize = async function (settings, callback) {
}
};
/**
* Callback method called by Strophe as the Strophe.Connection goes
* through various states while establishing or tearing down a
* connection.
* @method _converse#onConnectStatusChanged
* @private
* @memberOf _converse
*/
this.onConnectStatusChanged = function (status, message) {
/* Callback method called by Strophe as the Strophe.Connection goes
* through various states while establishing or tearing down a
* connection.
*/
_converse.log(`Status changed to: ${_converse.CONNECTION_STATUS[status]}`);
if (status === Strophe.Status.CONNECTED || status === Strophe.Status.ATTACHED) {
_converse.setConnectionStatus(status);
@ -1193,49 +1126,6 @@ _converse.initialize = async function (settings, callback) {
}
};
this.incrementMsgCounter = function () {
this.msg_counter += 1;
const unreadMsgCount = this.msg_counter;
let title = document.title;
if (!title) {
return;
}
if (title.search(/^Messages \(\d+\) /) === -1) {
title = `Messages (${unreadMsgCount}) ${title}`;
} else {
title = title.replace(/^Messages \(\d+\) /, `Messages (${unreadMsgCount})`);
}
};
this.clearMsgCounter = function () {
this.msg_counter = 0;
let title = document.title;
if (!title) {
return;
}
if (title.search(/^Messages \(\d+\) /) !== -1) {
title = title.replace(/^Messages \(\d+\) /, "");
}
};
this.initStatus = (reconnecting) => {
// If there's no xmppstatus obj, then we were never connected to
// begin with, so we set reconnecting to false.
reconnecting = _converse.xmppstatus === undefined ? false : reconnecting;
if (reconnecting) {
_converse.onStatusInitialized(reconnecting);
} else {
const id = `converse.xmppstatus-${_converse.bare_jid}`;
_converse.xmppstatus = new this.XMPPStatus({'id': id});
_converse.xmppstatus.browserStorage = _converse.createStore(id, "session");
_converse.xmppstatus.fetch({
'success': () => _converse.onStatusInitialized(reconnecting),
'error': () => _converse.onStatusInitialized(reconnecting),
'silent': true
});
}
}
this.saveWindowState = function (ev) {
// XXX: eventually we should be able to just use
// document.visibilityState (when we drop support for older
@ -1255,9 +1145,6 @@ _converse.initialize = async function (settings, callback) {
} else {
state = document.hidden ? "hidden" : "visible";
}
if (state === 'visible') {
_converse.clearMsgCounter();
}
_converse.windowState = state;
/**
* Triggered when window state has changed.
@ -1284,73 +1171,6 @@ _converse.initialize = async function (settings, callback) {
_converse.api.trigger('registeredGlobalEventHandlers');
};
this.enableCarbons = function () {
/* Ask the XMPP server to enable Message Carbons
* See XEP-0280 https://xmpp.org/extensions/xep-0280.html#enabling
*/
if (!this.message_carbons || !this.session || this.session.get('carbons_enabled')) {
return;
}
const carbons_iq = new Strophe.Builder('iq', {
'from': this.connection.jid,
'id': 'enablecarbons',
'type': 'set'
})
.c('enable', {xmlns: Strophe.NS.CARBONS});
this.connection.addHandler((iq) => {
if (iq.querySelectorAll('error').length > 0) {
_converse.log(
'An error occurred while trying to enable message carbons.',
Strophe.LogLevel.WARN);
} else {
this.session.save({'carbons_enabled': true});
_converse.log('Message carbons have been enabled.');
}
}, null, "iq", null, "enablecarbons");
this.connection.send(carbons_iq);
};
this.sendInitialPresence = function () {
if (_converse.send_initial_presence) {
_converse.xmppstatus.sendPresence();
}
};
this.onStatusInitialized = function (reconnecting) {
/**
* Triggered when the user's own chat status has been initialized.
* @event _converse#statusInitialized
* @example _converse.api.listen.on('statusInitialized', status => { ... });
* @example _converse.api.waitUntil('statusInitialized').then(() => { ... });
*/
_converse.api.trigger('statusInitialized', reconnecting);
if (reconnecting) {
/**
* After the connection has dropped and converse.js has reconnected.
* Any Strophe stanza handlers (as registered via `converse.listen.stanza`) will
* have to be registered anew.
* @event _converse#reconnected
* @example _converse.api.listen.on('reconnected', () => { ... });
*/
_converse.api.trigger('reconnected');
} else {
init_promise.resolve();
/**
* Triggered once converse.js has been initialized.
* See also {@link _converse#event:pluginsInitialized}.
* @event _converse#initialized
*/
_converse.api.trigger('initialized');
/**
* Triggered after the connection has been established and Converse
* has got all its ducks in a row.
* @event _converse#initialized
*/
_converse.api.trigger('connected');
}
};
this.bindResource = async function () {
/**
* Synchronous event triggered before we send an IQ to bind the user's
@ -1373,79 +1193,11 @@ _converse.initialize = async function (settings, callback) {
});
this.connfeedback = new this.ConnectionFeedback();
this.XMPPStatus = Backbone.Model.extend({
defaults: {
"status": _converse.default_state
},
initialize () {
this.on('change', item => {
if (!_.isObject(item.changed)) {
return;
}
if ('status' in item.changed || 'status_message' in item.changed) {
this.sendPresence(this.get('status'), this.get('status_message'));
}
});
},
getNickname () {
return _converse.nickname;
},
getFullname () {
// Gets overridden in converse-vcard
return '';
},
constructPresence (type, status_message) {
let presence;
type = _.isString(type) ? type : (this.get('status') || _converse.default_state);
status_message = _.isString(status_message) ? status_message : this.get('status_message');
// Most of these presence types are actually not explicitly sent,
// but I add all of them here for reference and future proofing.
if ((type === 'unavailable') ||
(type === 'probe') ||
(type === 'error') ||
(type === 'unsubscribe') ||
(type === 'unsubscribed') ||
(type === 'subscribe') ||
(type === 'subscribed')) {
presence = $pres({'type': type});
} else if (type === 'offline') {
presence = $pres({'type': 'unavailable'});
} else if (type === 'online') {
presence = $pres();
} else {
presence = $pres().c('show').t(type).up();
}
if (status_message) {
presence.c('status').t(status_message).up();
}
presence.c('priority').t(
_.isNaN(Number(_converse.priority)) ? 0 : _converse.priority
).up();
if (_converse.idle) {
const idle_since = new Date();
idle_since.setSeconds(idle_since.getSeconds() - _converse.idle_seconds);
presence.c('idle', {xmlns: Strophe.NS.IDLE, since: idle_since.toISOString()});
}
return presence;
},
sendPresence (type, status_message) {
_converse.api.send(this.constructPresence(type, status_message));
}
});
// Initialization
// --------------
await finishInitialization();
if (_converse.isTestEnv()) {
return _converse;
} else {
return init_promise;
}
};
@ -1671,70 +1423,6 @@ _converse.api = {
return promise;
},
/**
* Set and get the user's chat status, also called their *availability*.
*
* @namespace _converse.api.user.status
* @memberOf _converse.api.user
*/
status: {
/** Return the current user's availability status.
*
* @method _converse.api.user.status.get
* @example _converse.api.user.status.get();
*/
get () {
return _converse.xmppstatus.get('status');
},
/**
* The user's status can be set to one of the following values:
*
* @method _converse.api.user.status.set
* @param {string} value The user's chat status (e.g. 'away', 'dnd', 'offline', 'online', 'unavailable' or 'xa')
* @param {string} [message] A custom status message
*
* @example this._converse.api.user.status.set('dnd');
* @example this._converse.api.user.status.set('dnd', 'In a meeting');
*/
set (value, message) {
const data = {'status': value};
if (!_.includes(Object.keys(_converse.STATUS_WEIGHTS), value)) {
throw new Error(
'Invalid availability value. See https://xmpp.org/rfcs/rfc3921.html#rfc.section.2.2.2.1'
);
}
if (_.isString(message)) {
data.status_message = message;
}
_converse.xmppstatus.sendPresence(value);
_converse.xmppstatus.save(data);
},
/**
* Set and retrieve the user's custom status message.
*
* @namespace _converse.api.user.status.message
* @memberOf _converse.api.user.status
*/
message: {
/**
* @method _converse.api.user.status.message.get
* @returns {string} The status message
* @example const message = _converse.api.user.status.message.get()
*/
get () {
return _converse.xmppstatus.get('status_message');
},
/**
* @method _converse.api.user.status.message.set
* @param {string} status The status message
* @example _converse.api.user.status.message.set('In a meeting');
*/
set (status) {
_converse.xmppstatus.save({ status_message: status });
}
}
}
},
/**

View File

@ -6,6 +6,7 @@
/**
* @module converse-roster
*/
import "@converse/headless/converse-status";
import converse from "@converse/headless/converse-core";
const { Backbone, Strophe, $iq, $pres, dayjs, sizzle, _ } = converse.env;
@ -14,7 +15,7 @@ const u = converse.env.utils;
converse.plugins.add('converse-roster', {
dependencies: [],
dependencies: ['converse-status'],
initialize () {
/* The initialize function gets called as soon as the plugin is
@ -58,6 +59,21 @@ converse.plugins.add('converse-roster', {
};
/**
* Reject or cancel another user's subscription to our presence updates.
* @method rejectPresenceSubscription
* @private
* @memberOf _converse
* @param { String } jid - The Jabber ID of the user whose subscription is being canceled
* @param { String } message - An optional message to the user
*/
_converse.rejectPresenceSubscription = function (jid, message) {
const pres = $pres({to: jid, type: "unsubscribed"});
if (message && message !== "") { pres.c("status").t(message); }
_converse.api.send(pres);
};
/**
* Initialize the Bakcbone collections that represent the contats
* roster and the roster groups.
@ -91,6 +107,13 @@ converse.plugins.add('converse-roster', {
};
_converse.sendInitialPresence = function () {
if (_converse.send_initial_presence) {
_converse.xmppstatus.sendPresence();
}
};
/**
* Fetch all the roster groups, and then the roster contacts.
* Emit an event after fetching is done in each case.
@ -714,6 +737,7 @@ converse.plugins.add('converse-roster', {
_converse.api.trigger('contactRequest', this.create(user_data));
},
handleIncomingSubscription (presence) {
const jid = presence.getAttribute('from'),
bare_jid = Strophe.getBareJidFromJid(jid),
@ -972,6 +996,7 @@ converse.plugins.add('converse-roster', {
_converse.api.trigger('presencesInitialized', reconnecting);
});
_converse.api.listen.on('presencesInitialized', (reconnecting) => {
if (reconnecting) {
/**
@ -983,7 +1008,6 @@ converse.plugins.add('converse-roster', {
*/
_converse.api.trigger('rosterReadyAfterReconnection');
} else {
_converse.registerIntervalHandler();
_converse.initRoster();
}
_converse.roster.onConnected();

View File

@ -0,0 +1,306 @@
// Converse.js
// https://conversejs.org
//
// Copyright (c) 2013-2019, the Converse.js developers
// Licensed under the Mozilla Public License (MPLv2)
/**
* @module converse-status
*/
import { get, isNaN, isObject, isString } from "lodash";
import converse from "@converse/headless/converse-core";
const { Backbone, Strophe, $build, $pres } = converse.env;
converse.plugins.add('converse-status', {
initialize () {
const { _converse } = this;
_converse.XMPPStatus = Backbone.Model.extend({
defaults () {
return {"status": _converse.default_state}
},
initialize () {
this.on('change', item => {
if (!isObject(item.changed)) {
return;
}
if ('status' in item.changed || 'status_message' in item.changed) {
this.sendPresence(this.get('status'), this.get('status_message'));
}
});
},
getNickname () {
return _converse.nickname;
},
getFullname () {
// Gets overridden in converse-vcard
return '';
},
constructPresence (type, status_message) {
let presence;
type = isString(type) ? type : (this.get('status') || _converse.default_state);
status_message = isString(status_message) ? status_message : this.get('status_message');
// Most of these presence types are actually not explicitly sent,
// but I add all of them here for reference and future proofing.
if ((type === 'unavailable') ||
(type === 'probe') ||
(type === 'error') ||
(type === 'unsubscribe') ||
(type === 'unsubscribed') ||
(type === 'subscribe') ||
(type === 'subscribed')) {
presence = $pres({'type': type});
} else if (type === 'offline') {
presence = $pres({'type': 'unavailable'});
} else if (type === 'online') {
presence = $pres();
} else {
presence = $pres().c('show').t(type).up();
}
if (status_message) {
presence.c('status').t(status_message).up();
}
presence.c('priority').t(isNaN(Number(_converse.priority)) ? 0 : _converse.priority).up();
if (_converse.idle) {
const idle_since = new Date();
idle_since.setSeconds(idle_since.getSeconds() - _converse.idle_seconds);
presence.c('idle', {xmlns: Strophe.NS.IDLE, since: idle_since.toISOString()});
}
return presence;
},
sendPresence (type, status_message) {
_converse.api.send(this.constructPresence(type, status_message));
}
});
/**
* Send out a Client State Indication (XEP-0352)
* @private
* @method sendCSI
* @memberOf _converse
* @param { String } stat - The user's chat status
*/
_converse.sendCSI = function (stat) {
_converse.api.send($build(stat, {xmlns: Strophe.NS.CSI}));
_converse.inactive = (stat === _converse.INACTIVE) ? true : false;
};
_converse.onUserActivity = function () {
/* Resets counters and flags relating to CSI and auto_away/auto_xa */
if (_converse.idle_seconds > 0) {
_converse.idle_seconds = 0;
}
if (!get(_converse.connection, 'authenticated')) {
// We can't send out any stanzas when there's no authenticated connection.
// This can happen when the connection reconnects.
return;
}
if (_converse.inactive) {
_converse.sendCSI(_converse.ACTIVE);
}
if (_converse.idle) {
_converse.idle = false;
_converse.xmppstatus.sendPresence();
}
if (_converse.auto_changed_status === true) {
_converse.auto_changed_status = false;
// XXX: we should really remember the original state here, and
// then set it back to that...
_converse.xmppstatus.set('status', _converse.default_state);
}
};
_converse.onEverySecond = function () {
/* An interval handler running every second.
* Used for CSI and the auto_away and auto_xa features.
*/
if (!get(_converse.connection, 'authenticated')) {
// We can't send out any stanzas when there's no authenticated connection.
// This can happen when the connection reconnects.
return;
}
const stat = _converse.xmppstatus.get('status');
_converse.idle_seconds++;
if (_converse.csi_waiting_time > 0 &&
_converse.idle_seconds > _converse.csi_waiting_time &&
!_converse.inactive) {
_converse.sendCSI(_converse.INACTIVE);
}
if (_converse.idle_presence_timeout > 0 &&
_converse.idle_seconds > _converse.idle_presence_timeout &&
!_converse.idle) {
_converse.idle = true;
_converse.xmppstatus.sendPresence();
}
if (_converse.auto_away > 0 &&
_converse.idle_seconds > _converse.auto_away &&
stat !== 'away' && stat !== 'xa' && stat !== 'dnd') {
_converse.auto_changed_status = true;
_converse.xmppstatus.set('status', 'away');
} else if (_converse.auto_xa > 0 &&
_converse.idle_seconds > _converse.auto_xa &&
stat !== 'xa' && stat !== 'dnd') {
_converse.auto_changed_status = true;
_converse.xmppstatus.set('status', 'xa');
}
};
_converse.registerIntervalHandler = function () {
/* Set an interval of one second and register a handler for it.
* Required for the auto_away, auto_xa and csi_waiting_time features.
*/
if (
_converse.auto_away < 1 &&
_converse.auto_xa < 1 &&
_converse.csi_waiting_time < 1 &&
_converse.idle_presence_timeout < 1
) {
// Waiting time of less then one second means features aren't used.
return;
}
_converse.idle_seconds = 0;
_converse.auto_changed_status = false; // Was the user's status changed by Converse?
window.addEventListener('click', _converse.onUserActivity);
window.addEventListener('focus', _converse.onUserActivity);
window.addEventListener('keypress', _converse.onUserActivity);
window.addEventListener('mousemove', _converse.onUserActivity);
const options = {'once': true, 'passive': true};
window.addEventListener(_converse.unloadevent, _converse.onUserActivity, options);
window.addEventListener(_converse.unloadevent, () => {
if (_converse.session) {
_converse.session.save('active', false);
}
});
_converse.everySecondTrigger = window.setInterval(_converse.onEverySecond, 1000);
};
_converse.api.listen.on('presencesInitialized', (reconnecting) => {
if (!reconnecting) {
_converse.registerIntervalHandler();
}
});
function onStatusInitialized (reconnecting) {
/**
* Triggered when the user's own chat status has been initialized.
* @event _converse#statusInitialized
* @example _converse.api.listen.on('statusInitialized', status => { ... });
* @example _converse.api.waitUntil('statusInitialized').then(() => { ... });
*/
_converse.api.trigger('statusInitialized', reconnecting);
}
function initStatus (reconnecting) {
// If there's no xmppstatus obj, then we were never connected to
// begin with, so we set reconnecting to false.
reconnecting = _converse.xmppstatus === undefined ? false : reconnecting;
if (reconnecting) {
onStatusInitialized(reconnecting);
} else {
const id = `converse.xmppstatus-${_converse.bare_jid}`;
_converse.xmppstatus = new _converse.XMPPStatus({'id': id});
_converse.xmppstatus.browserStorage = _converse.createStore(id, "session");
_converse.xmppstatus.fetch({
'success': () => onStatusInitialized(reconnecting),
'error': () => onStatusInitialized(reconnecting),
'silent': true
});
}
}
/************************ BEGIN Event Handlers ************************/
_converse.api.listen.on('clearSession', () => {
if (_converse.shouldClearCache() && _converse.xmppstatus) {
_converse.xmppstatus.destroy();
_converse.xmppstatus.browserStorage._clear();
delete _converse.xmppstatus;
}
});
_converse.api.listen.on('connected', () => initStatus(false));
_converse.api.listen.on('reconnected', () => initStatus(true));
/************************ END Event Handlers ************************/
/************************ BEGIN API ************************/
Object.assign(_converse.api.user, {
/**
* Set and get the user's chat status, also called their *availability*.
*
* @namespace _converse.api.user.status
* @memberOf _converse.api.user
*/
status: {
/** Return the current user's availability status.
*
* @method _converse.api.user.status.get
* @example _converse.api.user.status.get();
*/
get () {
return _converse.xmppstatus.get('status');
},
/**
* The user's status can be set to one of the following values:
*
* @method _converse.api.user.status.set
* @param {string} value The user's chat status (e.g. 'away', 'dnd', 'offline', 'online', 'unavailable' or 'xa')
* @param {string} [message] A custom status message
*
* @example this._converse.api.user.status.set('dnd');
* @example this._converse.api.user.status.set('dnd', 'In a meeting');
*/
set (value, message) {
const data = {'status': value};
if (!Object.keys(_converse.STATUS_WEIGHTS).includes(value)) {
throw new Error(
'Invalid availability value. See https://xmpp.org/rfcs/rfc3921.html#rfc.section.2.2.2.1'
);
}
if (isString(message)) {
data.status_message = message;
}
_converse.xmppstatus.sendPresence(value);
_converse.xmppstatus.save(data);
},
/**
* Set and retrieve the user's custom status message.
*
* @namespace _converse.api.user.status.message
* @memberOf _converse.api.user.status
*/
message: {
/**
* @method _converse.api.user.status.message.get
* @returns {string} The status message
* @example const message = _converse.api.user.status.message.get()
*/
get () {
return _converse.xmppstatus.get('status_message');
},
/**
* @method _converse.api.user.status.message.set
* @param {string} status The status message
* @example _converse.api.user.status.message.set('In a meeting');
*/
set (status) {
_converse.xmppstatus.save({ status_message: status });
}
}
}
});
}
});

View File

@ -6,6 +6,7 @@
/**
* @module converse-vcard
*/
import "./converse-status";
import converse from "./converse-core";
import tpl_vcard from "./templates/vcard.html";
@ -15,7 +16,7 @@ const u = converse.env.utils;
converse.plugins.add('converse-vcard', {
dependencies: ["converse-roster"],
dependencies: ["converse-status", "converse-roster"],
overrides: {
XMPPStatus: {
@ -162,10 +163,9 @@ converse.plugins.add('converse-vcard', {
_converse.vcards.browserStorage = _converse.createStore(id, _converse.config.get('storage'));
_converse.vcards.fetch();
}
_converse.api.listen.on('afterResourceBinding', _converse.initVCardCollection);
_converse.api.listen.on('statusInitialized', () => {
_converse.initVCardCollection();
const vcards = _converse.vcards;
if (_converse.session) {
const jid = _converse.session.get('bare_jid');

View File

@ -14,6 +14,7 @@ import "./converse-pubsub"; // XEP-0060 Pubsub
import "./converse-roster"; // Contacts Roster
import "./converse-rsm"; // XEP-0059 Result Set management
import "./converse-smacks"; // XEP-0198 Stream Management
import "./converse-status"; // XEP-0199 XMPP Ping
import "./converse-vcard"; // XEP-0054 VCard-temp
/* END: Removable components */

View File

@ -256,6 +256,7 @@
if (el) {
el.parentElement.removeChild(el);
}
document.title = "Converse Tests";
done();
}
await Promise.all((promise_names || []).map(_converse.api.waitUntil));