Move roster models into their own module
This commit is contained in:
parent
09c55ebc28
commit
e5cfdca30d
@ -93,6 +93,7 @@ require.config({
|
|||||||
"converse-profile": "src/converse-profile",
|
"converse-profile": "src/converse-profile",
|
||||||
"converse-register": "src/converse-register",
|
"converse-register": "src/converse-register",
|
||||||
"converse-roomslist": "src/converse-roomslist",
|
"converse-roomslist": "src/converse-roomslist",
|
||||||
|
"converse-roster": "src/converse-roster",
|
||||||
"converse-rosterview": "src/converse-rosterview",
|
"converse-rosterview": "src/converse-rosterview",
|
||||||
"converse-singleton": "src/converse-singleton",
|
"converse-singleton": "src/converse-singleton",
|
||||||
"converse-vcard": "src/converse-vcard",
|
"converse-vcard": "src/converse-vcard",
|
||||||
|
@ -92,6 +92,7 @@
|
|||||||
'converse-profile',
|
'converse-profile',
|
||||||
'converse-register',
|
'converse-register',
|
||||||
'converse-roomslist',
|
'converse-roomslist',
|
||||||
|
'converse-roster',
|
||||||
'converse-rosterview',
|
'converse-rosterview',
|
||||||
'converse-singleton',
|
'converse-singleton',
|
||||||
'converse-spoilers',
|
'converse-spoilers',
|
||||||
@ -212,13 +213,8 @@
|
|||||||
|
|
||||||
const PROMISES = [
|
const PROMISES = [
|
||||||
'initialized',
|
'initialized',
|
||||||
'cachedRoster',
|
|
||||||
'connectionInitialized',
|
'connectionInitialized',
|
||||||
'pluginsInitialized',
|
'pluginsInitialized',
|
||||||
'roster',
|
|
||||||
'rosterContactsFetched',
|
|
||||||
'rosterGroupsFetched',
|
|
||||||
'rosterInitialized',
|
|
||||||
'statusInitialized'
|
'statusInitialized'
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -359,7 +355,6 @@
|
|||||||
registration_domain: '',
|
registration_domain: '',
|
||||||
rid: undefined,
|
rid: undefined,
|
||||||
root: window.document,
|
root: window.document,
|
||||||
roster_groups: true,
|
|
||||||
show_only_online_users: false,
|
show_only_online_users: false,
|
||||||
show_send_button: false,
|
show_send_button: false,
|
||||||
sid: undefined,
|
sid: undefined,
|
||||||
@ -679,9 +674,6 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.clearSession = function () {
|
this.clearSession = function () {
|
||||||
if (!_.isUndefined(this.roster)) {
|
|
||||||
this.roster.browserStorage._clear();
|
|
||||||
}
|
|
||||||
if (!_.isUndefined(this.session) && this.session.browserStorage) {
|
if (!_.isUndefined(this.session) && this.session.browserStorage) {
|
||||||
this.session.browserStorage._clear();
|
this.session.browserStorage._clear();
|
||||||
}
|
}
|
||||||
@ -779,51 +771,6 @@
|
|||||||
this.connection.send(carbons_iq);
|
this.connection.send(carbons_iq);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.initRoster = function () {
|
|
||||||
/* Initialize the Bakcbone collections that represent the contats
|
|
||||||
* roster and the roster groups.
|
|
||||||
*/
|
|
||||||
_converse.roster = new _converse.RosterContacts();
|
|
||||||
_converse.roster.browserStorage = new Backbone.BrowserStorage.session(
|
|
||||||
b64_sha1(`converse.contacts-${_converse.bare_jid}`));
|
|
||||||
_converse.rostergroups = new _converse.RosterGroups();
|
|
||||||
_converse.rostergroups.browserStorage = new Backbone.BrowserStorage.session(
|
|
||||||
b64_sha1(`converse.roster.groups${_converse.bare_jid}`));
|
|
||||||
_converse.emit('rosterInitialized');
|
|
||||||
};
|
|
||||||
|
|
||||||
this.populateRoster = function (ignore_cache=false) {
|
|
||||||
/* Fetch all the roster groups, and then the roster contacts.
|
|
||||||
* Emit an event after fetching is done in each case.
|
|
||||||
*
|
|
||||||
* Parameters:
|
|
||||||
* (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.
|
|
||||||
*/
|
|
||||||
if (ignore_cache) {
|
|
||||||
_converse.send_initial_presence = true;
|
|
||||||
_converse.roster.fetchFromServer()
|
|
||||||
.then(() => {
|
|
||||||
_converse.emit('rosterContactsFetched');
|
|
||||||
_converse.sendInitialPresence();
|
|
||||||
}).catch((reason) => {
|
|
||||||
_converse.log(reason, Strophe.LogLevel.ERROR);
|
|
||||||
_converse.sendInitialPresence();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
_converse.rostergroups.fetchRosterGroups().then(() => {
|
|
||||||
_converse.emit('rosterGroupsFetched');
|
|
||||||
return _converse.roster.fetchRosterContacts();
|
|
||||||
}).then(() => {
|
|
||||||
_converse.emit('rosterContactsFetched');
|
|
||||||
_converse.sendInitialPresence();
|
|
||||||
}).catch((reason) => {
|
|
||||||
_converse.log(reason, Strophe.LogLevel.ERROR);
|
|
||||||
_converse.sendInitialPresence();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.unregisterPresenceHandler = function () {
|
this.unregisterPresenceHandler = function () {
|
||||||
if (!_.isUndefined(_converse.presence_ref)) {
|
if (!_.isUndefined(_converse.presence_ref)) {
|
||||||
@ -832,16 +779,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.registerPresenceHandler = function () {
|
|
||||||
_converse.unregisterPresenceHandler();
|
|
||||||
_converse.presence_ref = _converse.connection.addHandler(
|
|
||||||
function (presence) {
|
|
||||||
_converse.roster.presenceHandler(presence);
|
|
||||||
return true;
|
|
||||||
}, null, 'presence', null);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
this.sendInitialPresence = function () {
|
this.sendInitialPresence = function () {
|
||||||
if (_converse.send_initial_presence) {
|
if (_converse.send_initial_presence) {
|
||||||
_converse.xmppstatus.sendPresence();
|
_converse.xmppstatus.sendPresence();
|
||||||
@ -849,24 +786,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.onStatusInitialized = function (reconnecting) {
|
this.onStatusInitialized = function (reconnecting) {
|
||||||
/* Continue with session establishment (e.g. fetching chat boxes,
|
_converse.emit('statusInitialized', reconnecting);
|
||||||
* populating the roster etc.) necessary once the connection has
|
|
||||||
* been established.
|
|
||||||
*/
|
|
||||||
_converse.emit('statusInitialized');
|
|
||||||
if (reconnecting) {
|
|
||||||
// No need to recreate the roster, otherwise we lose our
|
|
||||||
// cached data. However we still emit an event, to give
|
|
||||||
// event handlers a chance to register views for the
|
|
||||||
// roster and its groups, before we start populating.
|
|
||||||
_converse.emit('rosterReadyAfterReconnection');
|
|
||||||
} else {
|
|
||||||
_converse.registerIntervalHandler();
|
|
||||||
_converse.initRoster();
|
|
||||||
}
|
|
||||||
_converse.roster.onConnected();
|
|
||||||
_converse.populateRoster(reconnecting);
|
|
||||||
_converse.registerPresenceHandler();
|
|
||||||
if (reconnecting) {
|
if (reconnecting) {
|
||||||
_converse.emit('reconnected');
|
_converse.emit('reconnected');
|
||||||
} else {
|
} else {
|
||||||
@ -894,627 +814,6 @@
|
|||||||
_converse.initStatus(reconnecting)
|
_converse.initStatus(reconnecting)
|
||||||
};
|
};
|
||||||
|
|
||||||
this.RosterContact = Backbone.Model.extend({
|
|
||||||
|
|
||||||
defaults: {
|
|
||||||
'chat_state': undefined,
|
|
||||||
'chat_status': 'offline',
|
|
||||||
'image': _converse.DEFAULT_IMAGE,
|
|
||||||
'image_type': _converse.DEFAULT_IMAGE_TYPE,
|
|
||||||
'num_unread': 0,
|
|
||||||
'status': '',
|
|
||||||
},
|
|
||||||
|
|
||||||
initialize (attributes) {
|
|
||||||
const { jid } = attributes,
|
|
||||||
bare_jid = Strophe.getBareJidFromJid(jid).toLowerCase(),
|
|
||||||
resource = Strophe.getResourceFromJid(jid);
|
|
||||||
|
|
||||||
attributes.jid = bare_jid;
|
|
||||||
this.set(_.assignIn({
|
|
||||||
'fullname': bare_jid,
|
|
||||||
'groups': [],
|
|
||||||
'id': bare_jid,
|
|
||||||
'jid': bare_jid,
|
|
||||||
'resources': {},
|
|
||||||
'user_id': Strophe.getNodeFromJid(jid)
|
|
||||||
}, attributes));
|
|
||||||
|
|
||||||
this.on('change:chat_status', function (item) {
|
|
||||||
_converse.emit('contactStatusChanged', item.attributes);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
subscribe (message) {
|
|
||||||
/* Send a presence subscription request to this roster contact
|
|
||||||
*
|
|
||||||
* Parameters:
|
|
||||||
* (String) message - An optional message to explain the
|
|
||||||
* reason for the subscription request.
|
|
||||||
*/
|
|
||||||
const pres = $pres({to: this.get('jid'), type: "subscribe"});
|
|
||||||
if (message && message !== "") {
|
|
||||||
pres.c("status").t(message).up();
|
|
||||||
}
|
|
||||||
const nick = _converse.xmppstatus.get('nickname') || _converse.xmppstatus.get('fullname');
|
|
||||||
if (nick) {
|
|
||||||
pres.c('nick', {'xmlns': Strophe.NS.NICK}).t(nick).up();
|
|
||||||
}
|
|
||||||
_converse.connection.send(pres);
|
|
||||||
this.save('ask', "subscribe"); // ask === 'subscribe' Means we have asked to subscribe to them.
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
ackSubscribe () {
|
|
||||||
/* Upon receiving the presence stanza of type "subscribed",
|
|
||||||
* the user SHOULD acknowledge receipt of that subscription
|
|
||||||
* state notification by sending a presence stanza of type
|
|
||||||
* "subscribe" to the contact
|
|
||||||
*/
|
|
||||||
_converse.connection.send($pres({
|
|
||||||
'type': 'subscribe',
|
|
||||||
'to': this.get('jid')
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
ackUnsubscribe () {
|
|
||||||
/* Upon receiving the presence stanza of type "unsubscribed",
|
|
||||||
* the user SHOULD acknowledge receipt of that subscription state
|
|
||||||
* notification by sending a presence stanza of type "unsubscribe"
|
|
||||||
* this step lets the user's server know that it MUST no longer
|
|
||||||
* send notification of the subscription state change to the user.
|
|
||||||
* Parameters:
|
|
||||||
* (String) jid - The Jabber ID of the user who is unsubscribing
|
|
||||||
*/
|
|
||||||
_converse.connection.send($pres({'type': 'unsubscribe', 'to': this.get('jid')}));
|
|
||||||
this.removeFromRoster();
|
|
||||||
this.destroy();
|
|
||||||
},
|
|
||||||
|
|
||||||
unauthorize (message) {
|
|
||||||
/* Unauthorize this contact's presence subscription
|
|
||||||
* Parameters:
|
|
||||||
* (String) message - Optional message to send to the person being unauthorized
|
|
||||||
*/
|
|
||||||
_converse.rejectPresenceSubscription(this.get('jid'), message);
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
authorize (message) {
|
|
||||||
/* Authorize presence subscription
|
|
||||||
* Parameters:
|
|
||||||
* (String) message - Optional message to send to the person being authorized
|
|
||||||
*/
|
|
||||||
const pres = $pres({'to': this.get('jid'), 'type': "subscribed"});
|
|
||||||
if (message && message !== "") {
|
|
||||||
pres.c("status").t(message);
|
|
||||||
}
|
|
||||||
_converse.connection.send(pres);
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
addResource (presence) {
|
|
||||||
/* Adds a new resource and it's associated attributes as taken
|
|
||||||
* from the passed in presence stanza.
|
|
||||||
*
|
|
||||||
* Also updates the contact's chat_status if the presence has
|
|
||||||
* higher priority (and is newer).
|
|
||||||
*/
|
|
||||||
const jid = presence.getAttribute('from'),
|
|
||||||
chat_status = _.propertyOf(presence.querySelector('show'))('textContent') || 'online',
|
|
||||||
resource = Strophe.getResourceFromJid(jid),
|
|
||||||
delay = presence.querySelector(
|
|
||||||
`delay[xmlns="${Strophe.NS.DELAY}"]`
|
|
||||||
),
|
|
||||||
timestamp = _.isNull(delay) ? moment().format() : moment(delay.getAttribute('stamp')).format();
|
|
||||||
|
|
||||||
let priority = _.propertyOf(presence.querySelector('priority'))('textContent') || 0;
|
|
||||||
priority = _.isNaN(parseInt(priority, 10)) ? 0 : parseInt(priority, 10);
|
|
||||||
|
|
||||||
const resources = _.isObject(this.get('resources')) ? this.get('resources') : {};
|
|
||||||
resources[resource] = {
|
|
||||||
'name': resource,
|
|
||||||
'priority': priority,
|
|
||||||
'status': chat_status,
|
|
||||||
'timestamp': timestamp
|
|
||||||
};
|
|
||||||
const changed = {'resources': resources};
|
|
||||||
const hpr = this.getHighestPriorityResource();
|
|
||||||
if (priority == hpr.priority && timestamp == hpr.timestamp) {
|
|
||||||
// Only set the chat status if this is the newest resource
|
|
||||||
// with the highest priority
|
|
||||||
changed.chat_status = chat_status;
|
|
||||||
}
|
|
||||||
this.save(changed);
|
|
||||||
return resources;
|
|
||||||
},
|
|
||||||
|
|
||||||
removeResource (resource) {
|
|
||||||
/* Remove the passed in resource from the contact's resources map.
|
|
||||||
*
|
|
||||||
* Also recomputes the chat_status given that there's one less
|
|
||||||
* resource.
|
|
||||||
*/
|
|
||||||
let resources = this.get('resources');
|
|
||||||
if (!_.isObject(resources)) {
|
|
||||||
resources = {};
|
|
||||||
} else {
|
|
||||||
delete resources[resource];
|
|
||||||
}
|
|
||||||
this.save({
|
|
||||||
'resources': resources,
|
|
||||||
'chat_status': _.propertyOf(
|
|
||||||
this.getHighestPriorityResource())('status') || 'offline'
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getHighestPriorityResource () {
|
|
||||||
/* Return the resource with the highest priority.
|
|
||||||
*
|
|
||||||
* If multiple resources have the same priority, take the
|
|
||||||
* newest one.
|
|
||||||
*/
|
|
||||||
const resources = this.get('resources');
|
|
||||||
if (_.isObject(resources) && _.size(resources)) {
|
|
||||||
const val = _.flow(
|
|
||||||
_.values,
|
|
||||||
_.partial(_.sortBy, _, ['priority', 'timestamp']),
|
|
||||||
_.reverse
|
|
||||||
)(resources)[0];
|
|
||||||
if (!_.isUndefined(val)) {
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
removeFromRoster (callback, errback) {
|
|
||||||
/* Instruct the XMPP server to remove this contact from our roster
|
|
||||||
* Parameters:
|
|
||||||
* (Function) callback
|
|
||||||
*/
|
|
||||||
const iq = $iq({type: 'set'})
|
|
||||||
.c('query', {xmlns: Strophe.NS.ROSTER})
|
|
||||||
.c('item', {jid: this.get('jid'), subscription: "remove"});
|
|
||||||
_converse.connection.sendIQ(iq, callback, errback);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
this.RosterContacts = Backbone.Collection.extend({
|
|
||||||
model: _converse.RosterContact,
|
|
||||||
|
|
||||||
comparator (contact1, contact2) {
|
|
||||||
const status1 = contact1.get('chat_status') || 'offline';
|
|
||||||
const status2 = contact2.get('chat_status') || 'offline';
|
|
||||||
if (_converse.STATUS_WEIGHTS[status1] === _converse.STATUS_WEIGHTS[status2]) {
|
|
||||||
const name1 = (contact1.get('fullname') || contact1.get('jid')).toLowerCase();
|
|
||||||
const name2 = (contact2.get('fullname') || contact2.get('jid')).toLowerCase();
|
|
||||||
return name1 < name2 ? -1 : (name1 > name2? 1 : 0);
|
|
||||||
} else {
|
|
||||||
return _converse.STATUS_WEIGHTS[status1] < _converse.STATUS_WEIGHTS[status2] ? -1 : 1;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onConnected () {
|
|
||||||
/* Called as soon as the connection has been established
|
|
||||||
* (either after initial login, or after reconnection).
|
|
||||||
*
|
|
||||||
* Use the opportunity to register stanza handlers.
|
|
||||||
*/
|
|
||||||
this.registerRosterHandler();
|
|
||||||
this.registerRosterXHandler();
|
|
||||||
},
|
|
||||||
|
|
||||||
registerRosterHandler () {
|
|
||||||
/* Register a handler for roster IQ "set" stanzas, which update
|
|
||||||
* roster contacts.
|
|
||||||
*/
|
|
||||||
_converse.connection.addHandler(
|
|
||||||
_converse.roster.onRosterPush.bind(_converse.roster),
|
|
||||||
Strophe.NS.ROSTER, 'iq', "set"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
registerRosterXHandler () {
|
|
||||||
/* Register a handler for RosterX message stanzas, which are
|
|
||||||
* used to suggest roster contacts to a user.
|
|
||||||
*/
|
|
||||||
let t = 0;
|
|
||||||
_converse.connection.addHandler(
|
|
||||||
function (msg) {
|
|
||||||
window.setTimeout(
|
|
||||||
function () {
|
|
||||||
_converse.connection.flush();
|
|
||||||
_converse.roster.subscribeToSuggestedItems.bind(_converse.roster)(msg);
|
|
||||||
}, t);
|
|
||||||
t += msg.querySelectorAll('item').length*250;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
Strophe.NS.ROSTERX, 'message', null
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchRosterContacts () {
|
|
||||||
/* Fetches the roster contacts, first by trying the
|
|
||||||
* sessionStorage cache, and if that's empty, then by querying
|
|
||||||
* the XMPP server.
|
|
||||||
*
|
|
||||||
* Returns a promise which resolves once the contacts have been
|
|
||||||
* fetched.
|
|
||||||
*/
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.fetch({
|
|
||||||
'add': true,
|
|
||||||
'silent': true,
|
|
||||||
success (collection) {
|
|
||||||
if (collection.length === 0) {
|
|
||||||
_converse.send_initial_presence = true;
|
|
||||||
_converse.roster.fetchFromServer().then(resolve).catch(reject);
|
|
||||||
} else {
|
|
||||||
_converse.emit('cachedRoster', collection);
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
subscribeToSuggestedItems (msg) {
|
|
||||||
_.each(msg.querySelectorAll('item'), function (item) {
|
|
||||||
if (item.getAttribute('action') === 'add') {
|
|
||||||
_converse.roster.addAndSubscribe(
|
|
||||||
item.getAttribute('jid'),
|
|
||||||
_converse.xmppstatus.get('nickname') || _converse.xmppstatus.get('fullname')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
isSelf (jid) {
|
|
||||||
return u.isSameBareJID(jid, _converse.connection.jid);
|
|
||||||
},
|
|
||||||
|
|
||||||
addAndSubscribe (jid, name, groups, message, attributes) {
|
|
||||||
/* Add a roster contact and then once we have confirmation from
|
|
||||||
* the XMPP server we subscribe to that contact's presence updates.
|
|
||||||
* Parameters:
|
|
||||||
* (String) jid - The Jabber ID of the user being added and subscribed to.
|
|
||||||
* (String) name - The name of that user
|
|
||||||
* (Array of Strings) groups - Any roster groups the user might belong to
|
|
||||||
* (String) message - An optional message to explain the
|
|
||||||
* reason for the subscription request.
|
|
||||||
* (Object) attributes - Any additional attributes to be stored on the user's model.
|
|
||||||
*/
|
|
||||||
const handler = (contact) => {
|
|
||||||
if (contact instanceof _converse.RosterContact) {
|
|
||||||
contact.subscribe(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.addContactToRoster(jid, name, groups, attributes).then(handler, handler);
|
|
||||||
},
|
|
||||||
|
|
||||||
sendContactAddIQ (jid, name, groups, callback, errback) {
|
|
||||||
/* Send an IQ stanza to the XMPP server to add a new roster contact.
|
|
||||||
*
|
|
||||||
* Parameters:
|
|
||||||
* (String) jid - The Jabber ID of the user being added
|
|
||||||
* (String) name - The name of that user
|
|
||||||
* (Array of Strings) groups - Any roster groups the user might belong to
|
|
||||||
* (Function) callback - A function to call once the IQ is returned
|
|
||||||
* (Function) errback - A function to call if an error occured
|
|
||||||
*/
|
|
||||||
name = _.isEmpty(name)? jid: name;
|
|
||||||
const iq = $iq({type: 'set'})
|
|
||||||
.c('query', {xmlns: Strophe.NS.ROSTER})
|
|
||||||
.c('item', { jid, name });
|
|
||||||
_.each(groups, function (group) { iq.c('group').t(group).up(); });
|
|
||||||
_converse.connection.sendIQ(iq, callback, errback);
|
|
||||||
},
|
|
||||||
|
|
||||||
addContactToRoster (jid, name, groups, attributes) {
|
|
||||||
/* Adds a RosterContact instance to _converse.roster and
|
|
||||||
* registers the contact on the XMPP server.
|
|
||||||
* Returns a promise which is resolved once the XMPP server has
|
|
||||||
* responded.
|
|
||||||
*
|
|
||||||
* Parameters:
|
|
||||||
* (String) jid - The Jabber ID of the user being added and subscribed to.
|
|
||||||
* (String) name - The name of that user
|
|
||||||
* (Array of Strings) groups - Any roster groups the user might belong to
|
|
||||||
* (Object) attributes - Any additional attributes to be stored on the user's model.
|
|
||||||
*/
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
groups = groups || [];
|
|
||||||
name = _.isEmpty(name)? jid: name;
|
|
||||||
this.sendContactAddIQ(jid, name, groups,
|
|
||||||
() => {
|
|
||||||
const contact = this.create(_.assignIn({
|
|
||||||
'ask': undefined,
|
|
||||||
'fullname': name,
|
|
||||||
groups,
|
|
||||||
jid,
|
|
||||||
'requesting': false,
|
|
||||||
'subscription': 'none'
|
|
||||||
}, attributes), {sort: false});
|
|
||||||
resolve(contact);
|
|
||||||
},
|
|
||||||
function (err) {
|
|
||||||
alert(__('Sorry, there was an error while trying to add %1$s as a contact.', name));
|
|
||||||
_converse.log(err, Strophe.LogLevel.ERROR);
|
|
||||||
resolve(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
subscribeBack (bare_jid, presence) {
|
|
||||||
const contact = this.get(bare_jid);
|
|
||||||
if (contact instanceof _converse.RosterContact) {
|
|
||||||
contact.authorize().subscribe();
|
|
||||||
} else {
|
|
||||||
// Can happen when a subscription is retried or roster was deleted
|
|
||||||
const handler = (contact) => {
|
|
||||||
if (contact instanceof _converse.RosterContact) {
|
|
||||||
contact.authorize().subscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const nickname = _.get(sizzle(`nick[xmlns="${Strophe.NS.NICK}"]`, presence).pop(), 'textContent', null);
|
|
||||||
this.addContactToRoster(bare_jid, nickname, [], {'subscription': 'from'}).then(handler, handler);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getNumOnlineContacts () {
|
|
||||||
let ignored = ['offline', 'unavailable'];
|
|
||||||
if (_converse.show_only_online_users) {
|
|
||||||
ignored = _.union(ignored, ['dnd', 'xa', 'away']);
|
|
||||||
}
|
|
||||||
return _.sum(this.models.filter((model) => !_.includes(ignored, model.get('chat_status'))));
|
|
||||||
},
|
|
||||||
|
|
||||||
onRosterPush (iq) {
|
|
||||||
/* Handle roster updates from the XMPP server.
|
|
||||||
* See: https://xmpp.org/rfcs/rfc6121.html#roster-syntax-actions-push
|
|
||||||
*
|
|
||||||
* Parameters:
|
|
||||||
* (XMLElement) IQ - The IQ stanza received from the XMPP server.
|
|
||||||
*/
|
|
||||||
const id = iq.getAttribute('id');
|
|
||||||
const from = iq.getAttribute('from');
|
|
||||||
if (from && from !== "" && Strophe.getBareJidFromJid(from) !== _converse.bare_jid) {
|
|
||||||
// Receiving client MUST ignore stanza unless it has no from or from = user's bare JID.
|
|
||||||
// XXX: Some naughty servers apparently send from a full
|
|
||||||
// JID so we need to explicitly compare bare jids here.
|
|
||||||
// https://github.com/jcbrand/converse.js/issues/493
|
|
||||||
_converse.connection.send(
|
|
||||||
$iq({type: 'error', id, from: _converse.connection.jid})
|
|
||||||
.c('error', {'type': 'cancel'})
|
|
||||||
.c('service-unavailable', {'xmlns': Strophe.NS.ROSTER })
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
_converse.connection.send($iq({type: 'result', id, from: _converse.connection.jid}));
|
|
||||||
const items = sizzle(`query[xmlns="${Strophe.NS.ROSTER}"] item`, iq);
|
|
||||||
_.each(items, this.updateContact.bind(this));
|
|
||||||
_converse.emit('rosterPush', iq);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchFromServer () {
|
|
||||||
/* Fetch the roster from the XMPP server */
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const iq = $iq({
|
|
||||||
'type': 'get',
|
|
||||||
'id': _converse.connection.getUniqueId('roster')
|
|
||||||
}).c('query', {xmlns: Strophe.NS.ROSTER});
|
|
||||||
|
|
||||||
const callback = _.flow(this.onReceivedFromServer.bind(this), resolve);
|
|
||||||
const errback = function (iq) {
|
|
||||||
const errmsg = "Error while trying to fetch roster from the server";
|
|
||||||
_converse.log(errmsg, Strophe.LogLevel.ERROR);
|
|
||||||
reject(new Error(errmsg));
|
|
||||||
}
|
|
||||||
return _converse.connection.sendIQ(iq, callback, errback);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onReceivedFromServer (iq) {
|
|
||||||
/* An IQ stanza containing the roster has been received from
|
|
||||||
* the XMPP server.
|
|
||||||
*/
|
|
||||||
const items = sizzle(`query[xmlns="${Strophe.NS.ROSTER}"] item`, iq);
|
|
||||||
_.each(items, this.updateContact.bind(this));
|
|
||||||
_converse.emit('roster', iq);
|
|
||||||
},
|
|
||||||
|
|
||||||
updateContact (item) {
|
|
||||||
/* Update or create RosterContact models based on items
|
|
||||||
* received in the IQ from the server.
|
|
||||||
*/
|
|
||||||
const jid = item.getAttribute('jid');
|
|
||||||
if (this.isSelf(jid)) { return; }
|
|
||||||
|
|
||||||
const contact = this.get(jid),
|
|
||||||
subscription = item.getAttribute("subscription"),
|
|
||||||
ask = item.getAttribute("ask"),
|
|
||||||
groups = _.map(item.getElementsByTagName('group'), Strophe.getText);
|
|
||||||
|
|
||||||
if (!contact) {
|
|
||||||
if ((subscription === "none" && ask === null) || (subscription === "remove")) {
|
|
||||||
return; // We're lazy when adding contacts.
|
|
||||||
}
|
|
||||||
this.create({
|
|
||||||
'ask': ask,
|
|
||||||
'fullname': item.getAttribute("name") || jid,
|
|
||||||
'groups': groups,
|
|
||||||
'jid': jid,
|
|
||||||
'subscription': subscription
|
|
||||||
}, {sort: false});
|
|
||||||
} else {
|
|
||||||
if (subscription === "remove") {
|
|
||||||
return contact.destroy();
|
|
||||||
}
|
|
||||||
// We only find out about requesting contacts via the
|
|
||||||
// presence handler, so if we receive a contact
|
|
||||||
// here, we know they aren't requesting anymore.
|
|
||||||
// see docs/DEVELOPER.rst
|
|
||||||
contact.save({
|
|
||||||
'subscription': subscription,
|
|
||||||
'ask': ask,
|
|
||||||
'requesting': null,
|
|
||||||
'groups': groups
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
createRequestingContact (presence) {
|
|
||||||
/* Creates a Requesting Contact.
|
|
||||||
*
|
|
||||||
* Note: this method gets completely overridden by converse-vcard.js
|
|
||||||
*/
|
|
||||||
const bare_jid = Strophe.getBareJidFromJid(presence.getAttribute('from')),
|
|
||||||
nickname = _.get(sizzle(`nick[xmlns="${Strophe.NS.NICK}"]`, presence).pop(), 'textContent', null);
|
|
||||||
const user_data = {
|
|
||||||
'jid': bare_jid,
|
|
||||||
'subscription': 'none',
|
|
||||||
'ask': null,
|
|
||||||
'requesting': true,
|
|
||||||
'fullname': nickname
|
|
||||||
};
|
|
||||||
this.create(user_data);
|
|
||||||
_converse.emit('contactRequest', user_data);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleIncomingSubscription (presence) {
|
|
||||||
const jid = presence.getAttribute('from'),
|
|
||||||
bare_jid = Strophe.getBareJidFromJid(jid),
|
|
||||||
contact = this.get(bare_jid);
|
|
||||||
|
|
||||||
if (!_converse.allow_contact_requests) {
|
|
||||||
_converse.rejectPresenceSubscription(
|
|
||||||
jid,
|
|
||||||
__("This client does not allow presence subscriptions")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (_converse.auto_subscribe) {
|
|
||||||
if ((!contact) || (contact.get('subscription') !== 'to')) {
|
|
||||||
this.subscribeBack(bare_jid, presence);
|
|
||||||
} else {
|
|
||||||
contact.authorize();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (contact) {
|
|
||||||
if (contact.get('subscription') !== 'none') {
|
|
||||||
contact.authorize();
|
|
||||||
} else if (contact.get('ask') === "subscribe") {
|
|
||||||
contact.authorize();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.createRequestingContact(presence);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
presenceHandler (presence) {
|
|
||||||
const presence_type = presence.getAttribute('type');
|
|
||||||
if (presence_type === 'error') { return true; }
|
|
||||||
|
|
||||||
const jid = presence.getAttribute('from'),
|
|
||||||
bare_jid = Strophe.getBareJidFromJid(jid),
|
|
||||||
resource = Strophe.getResourceFromJid(jid),
|
|
||||||
chat_status = _.propertyOf(presence.querySelector('show'))('textContent') || 'online',
|
|
||||||
status_message = _.propertyOf(presence.querySelector('status'))('textContent'),
|
|
||||||
contact = this.get(bare_jid);
|
|
||||||
|
|
||||||
if (this.isSelf(bare_jid)) {
|
|
||||||
if ((_converse.connection.jid !== jid) &&
|
|
||||||
(presence_type !== 'unavailable') &&
|
|
||||||
(_converse.synchronize_availability === true ||
|
|
||||||
_converse.synchronize_availability === resource)) {
|
|
||||||
// Another resource has changed its status and
|
|
||||||
// synchronize_availability option set to update,
|
|
||||||
// we'll update ours as well.
|
|
||||||
_converse.xmppstatus.save({'status': chat_status});
|
|
||||||
if (status_message) {
|
|
||||||
_converse.xmppstatus.save({'status_message': status_message});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (_converse.jid === jid && presence_type === 'unavailable') {
|
|
||||||
// XXX: We've received an "unavailable" presence from our
|
|
||||||
// own resource. Apparently this happens due to a
|
|
||||||
// Prosody bug, whereby we send an IQ stanza to remove
|
|
||||||
// a roster contact, and Prosody then sends
|
|
||||||
// "unavailable" globally, instead of directed to the
|
|
||||||
// particular user that's removed.
|
|
||||||
//
|
|
||||||
// Here is the bug report: https://prosody.im/issues/1121
|
|
||||||
//
|
|
||||||
// I'm not sure whether this might legitimately happen
|
|
||||||
// in other cases.
|
|
||||||
//
|
|
||||||
// As a workaround for now we simply send our presence again,
|
|
||||||
// otherwise we're treated as offline.
|
|
||||||
_converse.xmppstatus.sendPresence();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} else if (sizzle(`query[xmlns="${Strophe.NS.MUC}"]`, presence).length) {
|
|
||||||
return; // Ignore MUC
|
|
||||||
}
|
|
||||||
if (contact && (status_message !== contact.get('status'))) {
|
|
||||||
contact.save({'status': status_message});
|
|
||||||
}
|
|
||||||
if (presence_type === 'subscribed' && contact) {
|
|
||||||
contact.ackSubscribe();
|
|
||||||
} else if (presence_type === 'unsubscribed' && contact) {
|
|
||||||
contact.ackUnsubscribe();
|
|
||||||
} else if (presence_type === 'unsubscribe') {
|
|
||||||
return;
|
|
||||||
} else if (presence_type === 'subscribe') {
|
|
||||||
this.handleIncomingSubscription(presence);
|
|
||||||
} else if (presence_type === 'unavailable' && contact) {
|
|
||||||
contact.removeResource(resource);
|
|
||||||
} else if (contact) {
|
|
||||||
// presence_type is undefined
|
|
||||||
contact.addResource(presence);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
this.RosterGroup = Backbone.Model.extend({
|
|
||||||
|
|
||||||
initialize (attributes) {
|
|
||||||
this.set(_.assignIn({
|
|
||||||
description: __('Click to hide these contacts'),
|
|
||||||
state: _converse.OPENED
|
|
||||||
}, attributes));
|
|
||||||
// Collection of contacts belonging to this group.
|
|
||||||
this.contacts = new _converse.RosterContacts();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
this.RosterGroups = Backbone.Collection.extend({
|
|
||||||
model: _converse.RosterGroup,
|
|
||||||
|
|
||||||
fetchRosterGroups () {
|
|
||||||
/* Fetches all the roster groups from sessionStorage.
|
|
||||||
*
|
|
||||||
* Returns a promise which resolves once the groups have been
|
|
||||||
* returned.
|
|
||||||
*/
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.fetch({
|
|
||||||
silent: true, // We need to first have all groups before
|
|
||||||
// we can start positioning them, so we set
|
|
||||||
// 'silent' to true.
|
|
||||||
success: resolve
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
this.ConnectionFeedback = Backbone.Model.extend({
|
this.ConnectionFeedback = Backbone.Model.extend({
|
||||||
defaults: {
|
defaults: {
|
||||||
@ -1816,9 +1115,6 @@
|
|||||||
*/
|
*/
|
||||||
_converse.emit('beforeTearDown');
|
_converse.emit('beforeTearDown');
|
||||||
_converse.unregisterPresenceHandler();
|
_converse.unregisterPresenceHandler();
|
||||||
if (_converse.roster) {
|
|
||||||
_converse.roster.off().reset(); // Removes roster contacts
|
|
||||||
}
|
|
||||||
if (!_.isUndefined(_converse.session)) {
|
if (!_.isUndefined(_converse.session)) {
|
||||||
_converse.session.destroy();
|
_converse.session.destroy();
|
||||||
}
|
}
|
||||||
@ -1981,25 +1277,6 @@
|
|||||||
_.each(promises, addPromise);
|
_.each(promises, addPromise);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'contacts': {
|
|
||||||
'get' (jids) {
|
|
||||||
const _getter = function (jid) {
|
|
||||||
return _converse.roster.get(Strophe.getBareJidFromJid(jid)) || null;
|
|
||||||
};
|
|
||||||
if (_.isUndefined(jids)) {
|
|
||||||
jids = _converse.roster.pluck('jid');
|
|
||||||
} else if (_.isString(jids)) {
|
|
||||||
return _getter(jids);
|
|
||||||
}
|
|
||||||
return _.map(jids, _getter);
|
|
||||||
},
|
|
||||||
'add' (jid, name) {
|
|
||||||
if (!_.isString(jid) || !_.includes(jid, '@')) {
|
|
||||||
throw new TypeError('contacts.add: invalid jid');
|
|
||||||
}
|
|
||||||
_converse.roster.addAndSubscribe(jid, _.isEmpty(name)? jid: name);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'tokens': {
|
'tokens': {
|
||||||
'get' (id) {
|
'get' (id) {
|
||||||
if (!_converse.expose_rid_and_sid || _.isUndefined(_converse.connection)) {
|
if (!_converse.expose_rid_and_sid || _.isUndefined(_converse.connection)) {
|
||||||
|
769
src/converse-roster.js
Normal file
769
src/converse-roster.js
Normal file
@ -0,0 +1,769 @@
|
|||||||
|
// Converse.js
|
||||||
|
// http://conversejs.org
|
||||||
|
//
|
||||||
|
// Copyright (c) 2012-2018, the Converse.js developers
|
||||||
|
// Licensed under the Mozilla Public License (MPLv2)
|
||||||
|
|
||||||
|
(function (root, factory) {
|
||||||
|
define(["converse-core"], factory);
|
||||||
|
}(this, function (converse) {
|
||||||
|
"use strict";
|
||||||
|
const { Backbone, Promise, Strophe, $iq, $pres, b64_sha1, moment, sizzle, _ } = converse.env;
|
||||||
|
const u = converse.env.utils;
|
||||||
|
|
||||||
|
converse.plugins.add('converse-roster', {
|
||||||
|
|
||||||
|
overrides: {
|
||||||
|
clearSession () {
|
||||||
|
this.__super__.clearSession.apply(this, arguments);
|
||||||
|
if (!_.isUndefined(this.roster)) {
|
||||||
|
this.roster.browserStorage._clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_tearDown () {
|
||||||
|
this.__super__._tearDown.apply(this, arguments);
|
||||||
|
if (this.roster) {
|
||||||
|
this.roster.off().reset(); // Removes roster contacts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize () {
|
||||||
|
/* The initialize function gets called as soon as the plugin is
|
||||||
|
* loaded by converse.js's plugin machinery.
|
||||||
|
*/
|
||||||
|
const { _converse } = this,
|
||||||
|
{ __ } = _converse;
|
||||||
|
|
||||||
|
_converse.api.promises.add([
|
||||||
|
'cachedRoster',
|
||||||
|
'roster',
|
||||||
|
'rosterContactsFetched',
|
||||||
|
'rosterGroupsFetched',
|
||||||
|
'rosterInitialized',
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
_converse.registerPresenceHandler = function () {
|
||||||
|
_converse.unregisterPresenceHandler();
|
||||||
|
_converse.presence_ref = _converse.connection.addHandler(
|
||||||
|
function (presence) {
|
||||||
|
_converse.roster.presenceHandler(presence);
|
||||||
|
return true;
|
||||||
|
}, null, 'presence', null);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
_converse.initRoster = function () {
|
||||||
|
/* Initialize the Bakcbone collections that represent the contats
|
||||||
|
* roster and the roster groups.
|
||||||
|
*/
|
||||||
|
_converse.roster = new _converse.RosterContacts();
|
||||||
|
_converse.roster.browserStorage = new Backbone.BrowserStorage.session(
|
||||||
|
b64_sha1(`converse.contacts-${_converse.bare_jid}`));
|
||||||
|
_converse.rostergroups = new _converse.RosterGroups();
|
||||||
|
_converse.rostergroups.browserStorage = new Backbone.BrowserStorage.session(
|
||||||
|
b64_sha1(`converse.roster.groups${_converse.bare_jid}`));
|
||||||
|
_converse.emit('rosterInitialized');
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
_converse.populateRoster = function (ignore_cache=false) {
|
||||||
|
/* Fetch all the roster groups, and then the roster contacts.
|
||||||
|
* Emit an event after fetching is done in each case.
|
||||||
|
*
|
||||||
|
* Parameters:
|
||||||
|
* (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.
|
||||||
|
*/
|
||||||
|
if (ignore_cache) {
|
||||||
|
_converse.send_initial_presence = true;
|
||||||
|
_converse.roster.fetchFromServer()
|
||||||
|
.then(() => {
|
||||||
|
_converse.emit('rosterContactsFetched');
|
||||||
|
_converse.sendInitialPresence();
|
||||||
|
}).catch((reason) => {
|
||||||
|
_converse.log(reason, Strophe.LogLevel.ERROR);
|
||||||
|
_converse.sendInitialPresence();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_converse.rostergroups.fetchRosterGroups().then(() => {
|
||||||
|
_converse.emit('rosterGroupsFetched');
|
||||||
|
return _converse.roster.fetchRosterContacts();
|
||||||
|
}).then(() => {
|
||||||
|
_converse.emit('rosterContactsFetched');
|
||||||
|
_converse.sendInitialPresence();
|
||||||
|
}).catch((reason) => {
|
||||||
|
_converse.log(reason, Strophe.LogLevel.ERROR);
|
||||||
|
_converse.sendInitialPresence();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
_converse.RosterContact = Backbone.Model.extend({
|
||||||
|
|
||||||
|
defaults: {
|
||||||
|
'chat_state': undefined,
|
||||||
|
'chat_status': 'offline',
|
||||||
|
'image': _converse.DEFAULT_IMAGE,
|
||||||
|
'image_type': _converse.DEFAULT_IMAGE_TYPE,
|
||||||
|
'num_unread': 0,
|
||||||
|
'status': '',
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize (attributes) {
|
||||||
|
const { jid } = attributes,
|
||||||
|
bare_jid = Strophe.getBareJidFromJid(jid).toLowerCase(),
|
||||||
|
resource = Strophe.getResourceFromJid(jid);
|
||||||
|
|
||||||
|
attributes.jid = bare_jid;
|
||||||
|
this.set(_.assignIn({
|
||||||
|
'fullname': bare_jid,
|
||||||
|
'groups': [],
|
||||||
|
'id': bare_jid,
|
||||||
|
'jid': bare_jid,
|
||||||
|
'resources': {},
|
||||||
|
'user_id': Strophe.getNodeFromJid(jid)
|
||||||
|
}, attributes));
|
||||||
|
|
||||||
|
this.on('change:chat_status', function (item) {
|
||||||
|
_converse.emit('contactStatusChanged', item.attributes);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
subscribe (message) {
|
||||||
|
/* Send a presence subscription request to this roster contact
|
||||||
|
*
|
||||||
|
* Parameters:
|
||||||
|
* (String) message - An optional message to explain the
|
||||||
|
* reason for the subscription request.
|
||||||
|
*/
|
||||||
|
const pres = $pres({to: this.get('jid'), type: "subscribe"});
|
||||||
|
if (message && message !== "") {
|
||||||
|
pres.c("status").t(message).up();
|
||||||
|
}
|
||||||
|
const nick = _converse.xmppstatus.get('nickname') || _converse.xmppstatus.get('fullname');
|
||||||
|
if (nick) {
|
||||||
|
pres.c('nick', {'xmlns': Strophe.NS.NICK}).t(nick).up();
|
||||||
|
}
|
||||||
|
_converse.connection.send(pres);
|
||||||
|
this.save('ask', "subscribe"); // ask === 'subscribe' Means we have asked to subscribe to them.
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
ackSubscribe () {
|
||||||
|
/* Upon receiving the presence stanza of type "subscribed",
|
||||||
|
* the user SHOULD acknowledge receipt of that subscription
|
||||||
|
* state notification by sending a presence stanza of type
|
||||||
|
* "subscribe" to the contact
|
||||||
|
*/
|
||||||
|
_converse.connection.send($pres({
|
||||||
|
'type': 'subscribe',
|
||||||
|
'to': this.get('jid')
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
ackUnsubscribe () {
|
||||||
|
/* Upon receiving the presence stanza of type "unsubscribed",
|
||||||
|
* the user SHOULD acknowledge receipt of that subscription state
|
||||||
|
* notification by sending a presence stanza of type "unsubscribe"
|
||||||
|
* this step lets the user's server know that it MUST no longer
|
||||||
|
* send notification of the subscription state change to the user.
|
||||||
|
* Parameters:
|
||||||
|
* (String) jid - The Jabber ID of the user who is unsubscribing
|
||||||
|
*/
|
||||||
|
_converse.connection.send($pres({'type': 'unsubscribe', 'to': this.get('jid')}));
|
||||||
|
this.removeFromRoster();
|
||||||
|
this.destroy();
|
||||||
|
},
|
||||||
|
|
||||||
|
unauthorize (message) {
|
||||||
|
/* Unauthorize this contact's presence subscription
|
||||||
|
* Parameters:
|
||||||
|
* (String) message - Optional message to send to the person being unauthorized
|
||||||
|
*/
|
||||||
|
_converse.rejectPresenceSubscription(this.get('jid'), message);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
authorize (message) {
|
||||||
|
/* Authorize presence subscription
|
||||||
|
* Parameters:
|
||||||
|
* (String) message - Optional message to send to the person being authorized
|
||||||
|
*/
|
||||||
|
const pres = $pres({'to': this.get('jid'), 'type': "subscribed"});
|
||||||
|
if (message && message !== "") {
|
||||||
|
pres.c("status").t(message);
|
||||||
|
}
|
||||||
|
_converse.connection.send(pres);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
addResource (presence) {
|
||||||
|
/* Adds a new resource and it's associated attributes as taken
|
||||||
|
* from the passed in presence stanza.
|
||||||
|
*
|
||||||
|
* Also updates the contact's chat_status if the presence has
|
||||||
|
* higher priority (and is newer).
|
||||||
|
*/
|
||||||
|
const jid = presence.getAttribute('from'),
|
||||||
|
chat_status = _.propertyOf(presence.querySelector('show'))('textContent') || 'online',
|
||||||
|
resource = Strophe.getResourceFromJid(jid),
|
||||||
|
delay = presence.querySelector(
|
||||||
|
`delay[xmlns="${Strophe.NS.DELAY}"]`
|
||||||
|
),
|
||||||
|
timestamp = _.isNull(delay) ? moment().format() : moment(delay.getAttribute('stamp')).format();
|
||||||
|
|
||||||
|
let priority = _.propertyOf(presence.querySelector('priority'))('textContent') || 0;
|
||||||
|
priority = _.isNaN(parseInt(priority, 10)) ? 0 : parseInt(priority, 10);
|
||||||
|
|
||||||
|
const resources = _.isObject(this.get('resources')) ? this.get('resources') : {};
|
||||||
|
resources[resource] = {
|
||||||
|
'name': resource,
|
||||||
|
'priority': priority,
|
||||||
|
'status': chat_status,
|
||||||
|
'timestamp': timestamp
|
||||||
|
};
|
||||||
|
const changed = {'resources': resources};
|
||||||
|
const hpr = this.getHighestPriorityResource();
|
||||||
|
if (priority == hpr.priority && timestamp == hpr.timestamp) {
|
||||||
|
// Only set the chat status if this is the newest resource
|
||||||
|
// with the highest priority
|
||||||
|
changed.chat_status = chat_status;
|
||||||
|
}
|
||||||
|
this.save(changed);
|
||||||
|
return resources;
|
||||||
|
},
|
||||||
|
|
||||||
|
removeResource (resource) {
|
||||||
|
/* Remove the passed in resource from the contact's resources map.
|
||||||
|
*
|
||||||
|
* Also recomputes the chat_status given that there's one less
|
||||||
|
* resource.
|
||||||
|
*/
|
||||||
|
let resources = this.get('resources');
|
||||||
|
if (!_.isObject(resources)) {
|
||||||
|
resources = {};
|
||||||
|
} else {
|
||||||
|
delete resources[resource];
|
||||||
|
}
|
||||||
|
this.save({
|
||||||
|
'resources': resources,
|
||||||
|
'chat_status': _.propertyOf(
|
||||||
|
this.getHighestPriorityResource())('status') || 'offline'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getHighestPriorityResource () {
|
||||||
|
/* Return the resource with the highest priority.
|
||||||
|
*
|
||||||
|
* If multiple resources have the same priority, take the
|
||||||
|
* newest one.
|
||||||
|
*/
|
||||||
|
const resources = this.get('resources');
|
||||||
|
if (_.isObject(resources) && _.size(resources)) {
|
||||||
|
const val = _.flow(
|
||||||
|
_.values,
|
||||||
|
_.partial(_.sortBy, _, ['priority', 'timestamp']),
|
||||||
|
_.reverse
|
||||||
|
)(resources)[0];
|
||||||
|
if (!_.isUndefined(val)) {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeFromRoster (callback, errback) {
|
||||||
|
/* Instruct the XMPP server to remove this contact from our roster
|
||||||
|
* Parameters:
|
||||||
|
* (Function) callback
|
||||||
|
*/
|
||||||
|
const iq = $iq({type: 'set'})
|
||||||
|
.c('query', {xmlns: Strophe.NS.ROSTER})
|
||||||
|
.c('item', {jid: this.get('jid'), subscription: "remove"});
|
||||||
|
_converse.connection.sendIQ(iq, callback, errback);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
_converse.RosterContacts = Backbone.Collection.extend({
|
||||||
|
model: _converse.RosterContact,
|
||||||
|
|
||||||
|
comparator (contact1, contact2) {
|
||||||
|
const status1 = contact1.get('chat_status') || 'offline';
|
||||||
|
const status2 = contact2.get('chat_status') || 'offline';
|
||||||
|
if (_converse.STATUS_WEIGHTS[status1] === _converse.STATUS_WEIGHTS[status2]) {
|
||||||
|
const name1 = (contact1.get('fullname') || contact1.get('jid')).toLowerCase();
|
||||||
|
const name2 = (contact2.get('fullname') || contact2.get('jid')).toLowerCase();
|
||||||
|
return name1 < name2 ? -1 : (name1 > name2? 1 : 0);
|
||||||
|
} else {
|
||||||
|
return _converse.STATUS_WEIGHTS[status1] < _converse.STATUS_WEIGHTS[status2] ? -1 : 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onConnected () {
|
||||||
|
/* Called as soon as the connection has been established
|
||||||
|
* (either after initial login, or after reconnection).
|
||||||
|
*
|
||||||
|
* Use the opportunity to register stanza handlers.
|
||||||
|
*/
|
||||||
|
this.registerRosterHandler();
|
||||||
|
this.registerRosterXHandler();
|
||||||
|
},
|
||||||
|
|
||||||
|
registerRosterHandler () {
|
||||||
|
/* Register a handler for roster IQ "set" stanzas, which update
|
||||||
|
* roster contacts.
|
||||||
|
*/
|
||||||
|
_converse.connection.addHandler(
|
||||||
|
_converse.roster.onRosterPush.bind(_converse.roster),
|
||||||
|
Strophe.NS.ROSTER, 'iq', "set"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
registerRosterXHandler () {
|
||||||
|
/* Register a handler for RosterX message stanzas, which are
|
||||||
|
* used to suggest roster contacts to a user.
|
||||||
|
*/
|
||||||
|
let t = 0;
|
||||||
|
_converse.connection.addHandler(
|
||||||
|
function (msg) {
|
||||||
|
window.setTimeout(
|
||||||
|
function () {
|
||||||
|
_converse.connection.flush();
|
||||||
|
_converse.roster.subscribeToSuggestedItems.bind(_converse.roster)(msg);
|
||||||
|
}, t);
|
||||||
|
t += msg.querySelectorAll('item').length*250;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
Strophe.NS.ROSTERX, 'message', null
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchRosterContacts () {
|
||||||
|
/* Fetches the roster contacts, first by trying the
|
||||||
|
* sessionStorage cache, and if that's empty, then by querying
|
||||||
|
* the XMPP server.
|
||||||
|
*
|
||||||
|
* Returns a promise which resolves once the contacts have been
|
||||||
|
* fetched.
|
||||||
|
*/
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.fetch({
|
||||||
|
'add': true,
|
||||||
|
'silent': true,
|
||||||
|
success (collection) {
|
||||||
|
if (collection.length === 0) {
|
||||||
|
_converse.send_initial_presence = true;
|
||||||
|
_converse.roster.fetchFromServer().then(resolve).catch(reject);
|
||||||
|
} else {
|
||||||
|
_converse.emit('cachedRoster', collection);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
subscribeToSuggestedItems (msg) {
|
||||||
|
_.each(msg.querySelectorAll('item'), function (item) {
|
||||||
|
if (item.getAttribute('action') === 'add') {
|
||||||
|
_converse.roster.addAndSubscribe(
|
||||||
|
item.getAttribute('jid'),
|
||||||
|
_converse.xmppstatus.get('nickname') || _converse.xmppstatus.get('fullname')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
isSelf (jid) {
|
||||||
|
return u.isSameBareJID(jid, _converse.connection.jid);
|
||||||
|
},
|
||||||
|
|
||||||
|
addAndSubscribe (jid, name, groups, message, attributes) {
|
||||||
|
/* Add a roster contact and then once we have confirmation from
|
||||||
|
* the XMPP server we subscribe to that contact's presence updates.
|
||||||
|
* Parameters:
|
||||||
|
* (String) jid - The Jabber ID of the user being added and subscribed to.
|
||||||
|
* (String) name - The name of that user
|
||||||
|
* (Array of Strings) groups - Any roster groups the user might belong to
|
||||||
|
* (String) message - An optional message to explain the
|
||||||
|
* reason for the subscription request.
|
||||||
|
* (Object) attributes - Any additional attributes to be stored on the user's model.
|
||||||
|
*/
|
||||||
|
const handler = (contact) => {
|
||||||
|
if (contact instanceof _converse.RosterContact) {
|
||||||
|
contact.subscribe(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.addContactToRoster(jid, name, groups, attributes).then(handler, handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
sendContactAddIQ (jid, name, groups, callback, errback) {
|
||||||
|
/* Send an IQ stanza to the XMPP server to add a new roster contact.
|
||||||
|
*
|
||||||
|
* Parameters:
|
||||||
|
* (String) jid - The Jabber ID of the user being added
|
||||||
|
* (String) name - The name of that user
|
||||||
|
* (Array of Strings) groups - Any roster groups the user might belong to
|
||||||
|
* (Function) callback - A function to call once the IQ is returned
|
||||||
|
* (Function) errback - A function to call if an error occured
|
||||||
|
*/
|
||||||
|
name = _.isEmpty(name)? jid: name;
|
||||||
|
const iq = $iq({type: 'set'})
|
||||||
|
.c('query', {xmlns: Strophe.NS.ROSTER})
|
||||||
|
.c('item', { jid, name });
|
||||||
|
_.each(groups, function (group) { iq.c('group').t(group).up(); });
|
||||||
|
_converse.connection.sendIQ(iq, callback, errback);
|
||||||
|
},
|
||||||
|
|
||||||
|
addContactToRoster (jid, name, groups, attributes) {
|
||||||
|
/* Adds a RosterContact instance to _converse.roster and
|
||||||
|
* registers the contact on the XMPP server.
|
||||||
|
* Returns a promise which is resolved once the XMPP server has
|
||||||
|
* responded.
|
||||||
|
*
|
||||||
|
* Parameters:
|
||||||
|
* (String) jid - The Jabber ID of the user being added and subscribed to.
|
||||||
|
* (String) name - The name of that user
|
||||||
|
* (Array of Strings) groups - Any roster groups the user might belong to
|
||||||
|
* (Object) attributes - Any additional attributes to be stored on the user's model.
|
||||||
|
*/
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
groups = groups || [];
|
||||||
|
name = _.isEmpty(name)? jid: name;
|
||||||
|
this.sendContactAddIQ(jid, name, groups,
|
||||||
|
() => {
|
||||||
|
const contact = this.create(_.assignIn({
|
||||||
|
'ask': undefined,
|
||||||
|
'fullname': name,
|
||||||
|
groups,
|
||||||
|
jid,
|
||||||
|
'requesting': false,
|
||||||
|
'subscription': 'none'
|
||||||
|
}, attributes), {sort: false});
|
||||||
|
resolve(contact);
|
||||||
|
},
|
||||||
|
function (err) {
|
||||||
|
alert(__('Sorry, there was an error while trying to add %1$s as a contact.', name));
|
||||||
|
_converse.log(err, Strophe.LogLevel.ERROR);
|
||||||
|
resolve(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
subscribeBack (bare_jid, presence) {
|
||||||
|
const contact = this.get(bare_jid);
|
||||||
|
if (contact instanceof _converse.RosterContact) {
|
||||||
|
contact.authorize().subscribe();
|
||||||
|
} else {
|
||||||
|
// Can happen when a subscription is retried or roster was deleted
|
||||||
|
const handler = (contact) => {
|
||||||
|
if (contact instanceof _converse.RosterContact) {
|
||||||
|
contact.authorize().subscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const nickname = _.get(sizzle(`nick[xmlns="${Strophe.NS.NICK}"]`, presence).pop(), 'textContent', null);
|
||||||
|
this.addContactToRoster(bare_jid, nickname, [], {'subscription': 'from'}).then(handler, handler);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getNumOnlineContacts () {
|
||||||
|
let ignored = ['offline', 'unavailable'];
|
||||||
|
if (_converse.show_only_online_users) {
|
||||||
|
ignored = _.union(ignored, ['dnd', 'xa', 'away']);
|
||||||
|
}
|
||||||
|
return _.sum(this.models.filter((model) => !_.includes(ignored, model.get('chat_status'))));
|
||||||
|
},
|
||||||
|
|
||||||
|
onRosterPush (iq) {
|
||||||
|
/* Handle roster updates from the XMPP server.
|
||||||
|
* See: https://xmpp.org/rfcs/rfc6121.html#roster-syntax-actions-push
|
||||||
|
*
|
||||||
|
* Parameters:
|
||||||
|
* (XMLElement) IQ - The IQ stanza received from the XMPP server.
|
||||||
|
*/
|
||||||
|
const id = iq.getAttribute('id');
|
||||||
|
const from = iq.getAttribute('from');
|
||||||
|
if (from && from !== "" && Strophe.getBareJidFromJid(from) !== _converse.bare_jid) {
|
||||||
|
// Receiving client MUST ignore stanza unless it has no from or from = user's bare JID.
|
||||||
|
// XXX: Some naughty servers apparently send from a full
|
||||||
|
// JID so we need to explicitly compare bare jids here.
|
||||||
|
// https://github.com/jcbrand/converse.js/issues/493
|
||||||
|
_converse.connection.send(
|
||||||
|
$iq({type: 'error', id, from: _converse.connection.jid})
|
||||||
|
.c('error', {'type': 'cancel'})
|
||||||
|
.c('service-unavailable', {'xmlns': Strophe.NS.ROSTER })
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_converse.connection.send($iq({type: 'result', id, from: _converse.connection.jid}));
|
||||||
|
const items = sizzle(`query[xmlns="${Strophe.NS.ROSTER}"] item`, iq);
|
||||||
|
_.each(items, this.updateContact.bind(this));
|
||||||
|
_converse.emit('rosterPush', iq);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchFromServer () {
|
||||||
|
/* Fetch the roster from the XMPP server */
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const iq = $iq({
|
||||||
|
'type': 'get',
|
||||||
|
'id': _converse.connection.getUniqueId('roster')
|
||||||
|
}).c('query', {xmlns: Strophe.NS.ROSTER});
|
||||||
|
|
||||||
|
const callback = _.flow(this.onReceivedFromServer.bind(this), resolve);
|
||||||
|
const errback = function (iq) {
|
||||||
|
const errmsg = "Error while trying to fetch roster from the server";
|
||||||
|
_converse.log(errmsg, Strophe.LogLevel.ERROR);
|
||||||
|
reject(new Error(errmsg));
|
||||||
|
}
|
||||||
|
return _converse.connection.sendIQ(iq, callback, errback);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onReceivedFromServer (iq) {
|
||||||
|
/* An IQ stanza containing the roster has been received from
|
||||||
|
* the XMPP server.
|
||||||
|
*/
|
||||||
|
const items = sizzle(`query[xmlns="${Strophe.NS.ROSTER}"] item`, iq);
|
||||||
|
_.each(items, this.updateContact.bind(this));
|
||||||
|
_converse.emit('roster', iq);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateContact (item) {
|
||||||
|
/* Update or create RosterContact models based on items
|
||||||
|
* received in the IQ from the server.
|
||||||
|
*/
|
||||||
|
const jid = item.getAttribute('jid');
|
||||||
|
if (this.isSelf(jid)) { return; }
|
||||||
|
|
||||||
|
const contact = this.get(jid),
|
||||||
|
subscription = item.getAttribute("subscription"),
|
||||||
|
ask = item.getAttribute("ask"),
|
||||||
|
groups = _.map(item.getElementsByTagName('group'), Strophe.getText);
|
||||||
|
|
||||||
|
if (!contact) {
|
||||||
|
if ((subscription === "none" && ask === null) || (subscription === "remove")) {
|
||||||
|
return; // We're lazy when adding contacts.
|
||||||
|
}
|
||||||
|
this.create({
|
||||||
|
'ask': ask,
|
||||||
|
'fullname': item.getAttribute("name") || jid,
|
||||||
|
'groups': groups,
|
||||||
|
'jid': jid,
|
||||||
|
'subscription': subscription
|
||||||
|
}, {sort: false});
|
||||||
|
} else {
|
||||||
|
if (subscription === "remove") {
|
||||||
|
return contact.destroy();
|
||||||
|
}
|
||||||
|
// We only find out about requesting contacts via the
|
||||||
|
// presence handler, so if we receive a contact
|
||||||
|
// here, we know they aren't requesting anymore.
|
||||||
|
// see docs/DEVELOPER.rst
|
||||||
|
contact.save({
|
||||||
|
'subscription': subscription,
|
||||||
|
'ask': ask,
|
||||||
|
'requesting': null,
|
||||||
|
'groups': groups
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
createRequestingContact (presence) {
|
||||||
|
const bare_jid = Strophe.getBareJidFromJid(presence.getAttribute('from')),
|
||||||
|
nickname = _.get(sizzle(`nick[xmlns="${Strophe.NS.NICK}"]`, presence).pop(), 'textContent', null);
|
||||||
|
const user_data = {
|
||||||
|
'jid': bare_jid,
|
||||||
|
'subscription': 'none',
|
||||||
|
'ask': null,
|
||||||
|
'requesting': true,
|
||||||
|
'fullname': nickname
|
||||||
|
};
|
||||||
|
this.create(user_data);
|
||||||
|
_converse.emit('contactRequest', user_data);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleIncomingSubscription (presence) {
|
||||||
|
const jid = presence.getAttribute('from'),
|
||||||
|
bare_jid = Strophe.getBareJidFromJid(jid),
|
||||||
|
contact = this.get(bare_jid);
|
||||||
|
|
||||||
|
if (!_converse.allow_contact_requests) {
|
||||||
|
_converse.rejectPresenceSubscription(
|
||||||
|
jid,
|
||||||
|
__("This client does not allow presence subscriptions")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (_converse.auto_subscribe) {
|
||||||
|
if ((!contact) || (contact.get('subscription') !== 'to')) {
|
||||||
|
this.subscribeBack(bare_jid, presence);
|
||||||
|
} else {
|
||||||
|
contact.authorize();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (contact) {
|
||||||
|
if (contact.get('subscription') !== 'none') {
|
||||||
|
contact.authorize();
|
||||||
|
} else if (contact.get('ask') === "subscribe") {
|
||||||
|
contact.authorize();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.createRequestingContact(presence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
presenceHandler (presence) {
|
||||||
|
const presence_type = presence.getAttribute('type');
|
||||||
|
if (presence_type === 'error') { return true; }
|
||||||
|
|
||||||
|
const jid = presence.getAttribute('from'),
|
||||||
|
bare_jid = Strophe.getBareJidFromJid(jid),
|
||||||
|
resource = Strophe.getResourceFromJid(jid),
|
||||||
|
chat_status = _.propertyOf(presence.querySelector('show'))('textContent') || 'online',
|
||||||
|
status_message = _.propertyOf(presence.querySelector('status'))('textContent'),
|
||||||
|
contact = this.get(bare_jid);
|
||||||
|
|
||||||
|
if (this.isSelf(bare_jid)) {
|
||||||
|
if ((_converse.connection.jid !== jid) &&
|
||||||
|
(presence_type !== 'unavailable') &&
|
||||||
|
(_converse.synchronize_availability === true ||
|
||||||
|
_converse.synchronize_availability === resource)) {
|
||||||
|
// Another resource has changed its status and
|
||||||
|
// synchronize_availability option set to update,
|
||||||
|
// we'll update ours as well.
|
||||||
|
_converse.xmppstatus.save({'status': chat_status});
|
||||||
|
if (status_message) {
|
||||||
|
_converse.xmppstatus.save({'status_message': status_message});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_converse.jid === jid && presence_type === 'unavailable') {
|
||||||
|
// XXX: We've received an "unavailable" presence from our
|
||||||
|
// own resource. Apparently this happens due to a
|
||||||
|
// Prosody bug, whereby we send an IQ stanza to remove
|
||||||
|
// a roster contact, and Prosody then sends
|
||||||
|
// "unavailable" globally, instead of directed to the
|
||||||
|
// particular user that's removed.
|
||||||
|
//
|
||||||
|
// Here is the bug report: https://prosody.im/issues/1121
|
||||||
|
//
|
||||||
|
// I'm not sure whether this might legitimately happen
|
||||||
|
// in other cases.
|
||||||
|
//
|
||||||
|
// As a workaround for now we simply send our presence again,
|
||||||
|
// otherwise we're treated as offline.
|
||||||
|
_converse.xmppstatus.sendPresence();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else if (sizzle(`query[xmlns="${Strophe.NS.MUC}"]`, presence).length) {
|
||||||
|
return; // Ignore MUC
|
||||||
|
}
|
||||||
|
if (contact && (status_message !== contact.get('status'))) {
|
||||||
|
contact.save({'status': status_message});
|
||||||
|
}
|
||||||
|
if (presence_type === 'subscribed' && contact) {
|
||||||
|
contact.ackSubscribe();
|
||||||
|
} else if (presence_type === 'unsubscribed' && contact) {
|
||||||
|
contact.ackUnsubscribe();
|
||||||
|
} else if (presence_type === 'unsubscribe') {
|
||||||
|
return;
|
||||||
|
} else if (presence_type === 'subscribe') {
|
||||||
|
this.handleIncomingSubscription(presence);
|
||||||
|
} else if (presence_type === 'unavailable' && contact) {
|
||||||
|
contact.removeResource(resource);
|
||||||
|
} else if (contact) {
|
||||||
|
// presence_type is undefined
|
||||||
|
contact.addResource(presence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
_converse.RosterGroup = Backbone.Model.extend({
|
||||||
|
|
||||||
|
initialize (attributes) {
|
||||||
|
this.set(_.assignIn({
|
||||||
|
description: __('Click to hide these contacts'),
|
||||||
|
state: _converse.OPENED
|
||||||
|
}, attributes));
|
||||||
|
// Collection of contacts belonging to this group.
|
||||||
|
this.contacts = new _converse.RosterContacts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
_converse.RosterGroups = Backbone.Collection.extend({
|
||||||
|
model: _converse.RosterGroup,
|
||||||
|
|
||||||
|
fetchRosterGroups () {
|
||||||
|
/* Fetches all the roster groups from sessionStorage.
|
||||||
|
*
|
||||||
|
* Returns a promise which resolves once the groups have been
|
||||||
|
* returned.
|
||||||
|
*/
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.fetch({
|
||||||
|
silent: true, // We need to first have all groups before
|
||||||
|
// we can start positioning them, so we set
|
||||||
|
// 'silent' to true.
|
||||||
|
success: resolve
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/********** Event Handlers *************/
|
||||||
|
|
||||||
|
_converse.api.listen.on('statusInitialized', (reconnecting) => {
|
||||||
|
if (reconnecting) {
|
||||||
|
// No need to recreate the roster, otherwise we lose our
|
||||||
|
// cached data. However we still emit an event, to give
|
||||||
|
// event handlers a chance to register views for the
|
||||||
|
// roster and its groups, before we start populating.
|
||||||
|
_converse.emit('rosterReadyAfterReconnection');
|
||||||
|
} else {
|
||||||
|
_converse.registerIntervalHandler();
|
||||||
|
_converse.initRoster();
|
||||||
|
}
|
||||||
|
_converse.roster.onConnected();
|
||||||
|
_converse.populateRoster(reconnecting);
|
||||||
|
_converse.registerPresenceHandler();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/************************ API ************************/
|
||||||
|
// API methods only available to plugins
|
||||||
|
|
||||||
|
_.extend(_converse.api, {
|
||||||
|
'contacts': {
|
||||||
|
'get' (jids) {
|
||||||
|
const _getter = function (jid) {
|
||||||
|
return _converse.roster.get(Strophe.getBareJidFromJid(jid)) || null;
|
||||||
|
};
|
||||||
|
if (_.isUndefined(jids)) {
|
||||||
|
jids = _converse.roster.pluck('jid');
|
||||||
|
} else if (_.isString(jids)) {
|
||||||
|
return _getter(jids);
|
||||||
|
}
|
||||||
|
return _.map(jids, _getter);
|
||||||
|
},
|
||||||
|
'add' (jid, name) {
|
||||||
|
if (!_.isString(jid) || !_.includes(jid, '@')) {
|
||||||
|
throw new TypeError('contacts.add: invalid jid');
|
||||||
|
}
|
||||||
|
_converse.roster.addAndSubscribe(jid, _.isEmpty(name)? jid: name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}));
|
@ -1,10 +1,8 @@
|
|||||||
// Converse.js (A browser based XMPP chat client)
|
// Converse.js
|
||||||
// http://conversejs.org
|
// http://conversejs.org
|
||||||
//
|
//
|
||||||
// Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
|
// Copyright (c) 2012-2018, the Converse.js developers
|
||||||
// Licensed under the Mozilla Public License (MPLv2)
|
// Licensed under the Mozilla Public License (MPLv2)
|
||||||
//
|
|
||||||
/*global define */
|
|
||||||
|
|
||||||
(function (root, factory) {
|
(function (root, factory) {
|
||||||
define(["converse-core",
|
define(["converse-core",
|
||||||
@ -39,7 +37,7 @@
|
|||||||
|
|
||||||
converse.plugins.add('converse-rosterview', {
|
converse.plugins.add('converse-rosterview', {
|
||||||
|
|
||||||
dependencies: ["converse-modal"],
|
dependencies: ["converse-roster", "converse-modal"],
|
||||||
|
|
||||||
overrides: {
|
overrides: {
|
||||||
// Overrides mentioned here will be picked up by converse.js's
|
// Overrides mentioned here will be picked up by converse.js's
|
||||||
@ -82,6 +80,7 @@
|
|||||||
_converse.api.settings.update({
|
_converse.api.settings.update({
|
||||||
'allow_chat_pending_contacts': true,
|
'allow_chat_pending_contacts': true,
|
||||||
'allow_contact_removal': true,
|
'allow_contact_removal': true,
|
||||||
|
'roster_groups': true,
|
||||||
'show_toolbar': true,
|
'show_toolbar': true,
|
||||||
'xhr_user_search_url': null
|
'xhr_user_search_url': null
|
||||||
});
|
});
|
||||||
|
@ -63,6 +63,10 @@
|
|||||||
|
|
||||||
converse.plugins.add('converse-vcard', {
|
converse.plugins.add('converse-vcard', {
|
||||||
|
|
||||||
|
// FIXME: After refactoring, the dependency switches, from
|
||||||
|
// converse-roster to converse-vcard
|
||||||
|
dependencies: ["converse-roster"],
|
||||||
|
|
||||||
overrides: {
|
overrides: {
|
||||||
// Overrides mentioned here will be picked up by converse.js's
|
// Overrides mentioned here will be picked up by converse.js's
|
||||||
// plugin architecture they will replace existing methods on the
|
// plugin architecture they will replace existing methods on the
|
||||||
|
@ -22,6 +22,7 @@ if (typeof define !== 'undefined') {
|
|||||||
"converse-notification", // HTML5 Notifications
|
"converse-notification", // HTML5 Notifications
|
||||||
"converse-otr", // Off-the-record encryption for one-on-one messages
|
"converse-otr", // Off-the-record encryption for one-on-one messages
|
||||||
"converse-ping", // XEP-0199 XMPP Ping
|
"converse-ping", // XEP-0199 XMPP Ping
|
||||||
|
"converse-roster",
|
||||||
"converse-register", // XEP-0077 In-band registration
|
"converse-register", // XEP-0077 In-band registration
|
||||||
"converse-roomslist", // Show currently open chat rooms
|
"converse-roomslist", // Show currently open chat rooms
|
||||||
"converse-vcard", // XEP-0054 VCard-temp
|
"converse-vcard", // XEP-0054 VCard-temp
|
||||||
|
Loading…
Reference in New Issue
Block a user