Move roster view code into a separate plugin
This commit is contained in:
parent
f55b593791
commit
df3bcad0b3
|
@ -46,6 +46,7 @@ require.config({
|
||||||
// Converse
|
// Converse
|
||||||
"converse-api": "src/converse-api",
|
"converse-api": "src/converse-api",
|
||||||
"converse-chatview": "src/converse-chatview",
|
"converse-chatview": "src/converse-chatview",
|
||||||
|
"converse-rosterview": "src/converse-rosterview",
|
||||||
"converse-controlbox": "src/converse-controlbox",
|
"converse-controlbox": "src/converse-controlbox",
|
||||||
"converse-core": "src/converse-core",
|
"converse-core": "src/converse-core",
|
||||||
"converse-headline": "src/converse-headline",
|
"converse-headline": "src/converse-headline",
|
||||||
|
|
|
@ -10,15 +10,14 @@
|
||||||
define("converse-controlbox", [
|
define("converse-controlbox", [
|
||||||
"converse-core",
|
"converse-core",
|
||||||
"converse-api",
|
"converse-api",
|
||||||
|
// TODO: remove the next two dependencies
|
||||||
"converse-rosterview",
|
"converse-rosterview",
|
||||||
// TODO: remove this dependency
|
|
||||||
"converse-chatview"
|
"converse-chatview"
|
||||||
], factory);
|
], factory);
|
||||||
}(this, function (converse, converse_api) {
|
}(this, function (converse, converse_api) {
|
||||||
"use strict";
|
"use strict";
|
||||||
// Strophe methods for building stanzas
|
// Strophe methods for building stanzas
|
||||||
var Strophe = converse_api.env.Strophe,
|
var Strophe = converse_api.env.Strophe,
|
||||||
$iq = converse_api.env.$iq,
|
|
||||||
b64_sha1 = converse_api.env.b64_sha1,
|
b64_sha1 = converse_api.env.b64_sha1,
|
||||||
utils = converse_api.env.utils;
|
utils = converse_api.env.utils;
|
||||||
// Other necessary globals
|
// Other necessary globals
|
||||||
|
@ -177,26 +176,7 @@
|
||||||
show_controlbox_by_default: false,
|
show_controlbox_by_default: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
var STATUSES = {
|
|
||||||
'dnd': __('This contact is busy'),
|
|
||||||
'online': __('This contact is online'),
|
|
||||||
'offline': __('This contact is offline'),
|
|
||||||
'unavailable': __('This contact is unavailable'),
|
|
||||||
'xa': __('This contact is away for an extended period'),
|
|
||||||
'away': __('This contact is away')
|
|
||||||
};
|
|
||||||
var DESC_GROUP_TOGGLE = __('Click to hide these contacts');
|
|
||||||
var LABEL_CONTACTS = __('Contacts');
|
var LABEL_CONTACTS = __('Contacts');
|
||||||
var LABEL_GROUPS = __('Groups');
|
|
||||||
var HEADER_CURRENT_CONTACTS = __('My contacts');
|
|
||||||
var HEADER_PENDING_CONTACTS = __('Pending contacts');
|
|
||||||
var HEADER_REQUESTING_CONTACTS = __('Contact requests');
|
|
||||||
var HEADER_UNGROUPED = __('Ungrouped');
|
|
||||||
var HEADER_WEIGHTS = {};
|
|
||||||
HEADER_WEIGHTS[HEADER_CURRENT_CONTACTS] = 0;
|
|
||||||
HEADER_WEIGHTS[HEADER_UNGROUPED] = 1;
|
|
||||||
HEADER_WEIGHTS[HEADER_REQUESTING_CONTACTS] = 2;
|
|
||||||
HEADER_WEIGHTS[HEADER_PENDING_CONTACTS] = 3;
|
|
||||||
|
|
||||||
converse.addControlBox = function () {
|
converse.addControlBox = function () {
|
||||||
return converse.chatboxes.add({
|
return converse.chatboxes.add({
|
||||||
|
@ -693,700 +673,6 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
converse.RosterContactView = Backbone.View.extend({
|
|
||||||
tagName: 'dd',
|
|
||||||
|
|
||||||
events: {
|
|
||||||
"click .accept-xmpp-request": "acceptRequest",
|
|
||||||
"click .decline-xmpp-request": "declineRequest",
|
|
||||||
"click .open-chat": "openChat",
|
|
||||||
"click .remove-xmpp-contact": "removeContact"
|
|
||||||
},
|
|
||||||
|
|
||||||
initialize: function () {
|
|
||||||
this.model.on("change", this.render, this);
|
|
||||||
this.model.on("remove", this.remove, this);
|
|
||||||
this.model.on("destroy", this.remove, this);
|
|
||||||
this.model.on("open", this.openChat, this);
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function () {
|
|
||||||
if (!this.model.showInRoster()) {
|
|
||||||
this.$el.hide();
|
|
||||||
return this;
|
|
||||||
} else if (this.$el[0].style.display === "none") {
|
|
||||||
this.$el.show();
|
|
||||||
}
|
|
||||||
var item = this.model,
|
|
||||||
ask = item.get('ask'),
|
|
||||||
chat_status = item.get('chat_status'),
|
|
||||||
requesting = item.get('requesting'),
|
|
||||||
subscription = item.get('subscription');
|
|
||||||
|
|
||||||
var classes_to_remove = [
|
|
||||||
'current-xmpp-contact',
|
|
||||||
'pending-xmpp-contact',
|
|
||||||
'requesting-xmpp-contact'
|
|
||||||
].concat(_.keys(STATUSES));
|
|
||||||
|
|
||||||
_.each(classes_to_remove,
|
|
||||||
function (cls) {
|
|
||||||
if (this.el.className.indexOf(cls) !== -1) {
|
|
||||||
this.$el.removeClass(cls);
|
|
||||||
}
|
|
||||||
}, this);
|
|
||||||
this.$el.addClass(chat_status).data('status', chat_status);
|
|
||||||
|
|
||||||
if ((ask === 'subscribe') || (subscription === 'from')) {
|
|
||||||
/* ask === 'subscribe'
|
|
||||||
* Means we have asked to subscribe to them.
|
|
||||||
*
|
|
||||||
* subscription === 'from'
|
|
||||||
* They are subscribed to use, but not vice versa.
|
|
||||||
* We assume that there is a pending subscription
|
|
||||||
* from us to them (otherwise we're in a state not
|
|
||||||
* supported by converse.js).
|
|
||||||
*
|
|
||||||
* So in both cases the user is a "pending" contact.
|
|
||||||
*/
|
|
||||||
this.$el.addClass('pending-xmpp-contact');
|
|
||||||
this.$el.html(converse.templates.pending_contact(
|
|
||||||
_.extend(item.toJSON(), {
|
|
||||||
'desc_remove': __('Click to remove this contact'),
|
|
||||||
'allow_chat_pending_contacts': converse.allow_chat_pending_contacts
|
|
||||||
})
|
|
||||||
));
|
|
||||||
} else if (requesting === true) {
|
|
||||||
this.$el.addClass('requesting-xmpp-contact');
|
|
||||||
this.$el.html(converse.templates.requesting_contact(
|
|
||||||
_.extend(item.toJSON(), {
|
|
||||||
'desc_accept': __("Click to accept this contact request"),
|
|
||||||
'desc_decline': __("Click to decline this contact request"),
|
|
||||||
'allow_chat_pending_contacts': converse.allow_chat_pending_contacts
|
|
||||||
})
|
|
||||||
));
|
|
||||||
converse.controlboxtoggle.showControlBox();
|
|
||||||
} else if (subscription === 'both' || subscription === 'to') {
|
|
||||||
this.$el.addClass('current-xmpp-contact');
|
|
||||||
this.$el.removeClass(_.without(['both', 'to'], subscription)[0]).addClass(subscription);
|
|
||||||
this.$el.html(converse.templates.roster_item(
|
|
||||||
_.extend(item.toJSON(), {
|
|
||||||
'desc_status': STATUSES[chat_status||'offline'],
|
|
||||||
'desc_chat': __('Click to chat with this contact'),
|
|
||||||
'desc_remove': __('Click to remove this contact'),
|
|
||||||
'title_fullname': __('Name'),
|
|
||||||
'allow_contact_removal': converse.allow_contact_removal
|
|
||||||
})
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
openChat: function (ev) {
|
|
||||||
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
|
||||||
return converse.chatboxviews.showChat(this.model.attributes);
|
|
||||||
},
|
|
||||||
|
|
||||||
removeContact: function (ev) {
|
|
||||||
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
|
||||||
if (!converse.allow_contact_removal) { return; }
|
|
||||||
var result = confirm(__("Are you sure you want to remove this contact?"));
|
|
||||||
if (result === true) {
|
|
||||||
var iq = $iq({type: 'set'})
|
|
||||||
.c('query', {xmlns: Strophe.NS.ROSTER})
|
|
||||||
.c('item', {jid: this.model.get('jid'), subscription: "remove"});
|
|
||||||
converse.connection.sendIQ(iq,
|
|
||||||
function (iq) {
|
|
||||||
this.model.destroy();
|
|
||||||
this.remove();
|
|
||||||
}.bind(this),
|
|
||||||
function (err) {
|
|
||||||
alert(__("Sorry, there was an error while trying to remove "+name+" as a contact."));
|
|
||||||
converse.log(err);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
acceptRequest: function (ev) {
|
|
||||||
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
|
||||||
converse.roster.sendContactAddIQ(
|
|
||||||
this.model.get('jid'),
|
|
||||||
this.model.get('fullname'),
|
|
||||||
[],
|
|
||||||
function () { this.model.authorize().subscribe(); }.bind(this)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
declineRequest: function (ev) {
|
|
||||||
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
|
||||||
var result = confirm(__("Are you sure you want to decline this contact request?"));
|
|
||||||
if (result === true) {
|
|
||||||
this.model.unauthorize().destroy();
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
converse.RosterGroup = Backbone.Model.extend({
|
|
||||||
initialize: function (attributes, options) {
|
|
||||||
this.set(_.extend({
|
|
||||||
description: DESC_GROUP_TOGGLE,
|
|
||||||
state: converse.OPENED
|
|
||||||
}, attributes));
|
|
||||||
// Collection of contacts belonging to this group.
|
|
||||||
this.contacts = new converse.RosterContacts();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
converse.RosterGroupView = Backbone.Overview.extend({
|
|
||||||
tagName: 'dt',
|
|
||||||
className: 'roster-group',
|
|
||||||
events: {
|
|
||||||
"click a.group-toggle": "toggle"
|
|
||||||
},
|
|
||||||
|
|
||||||
initialize: function () {
|
|
||||||
this.model.contacts.on("add", this.addContact, this);
|
|
||||||
this.model.contacts.on("change:subscription", this.onContactSubscriptionChange, this);
|
|
||||||
this.model.contacts.on("change:requesting", this.onContactRequestChange, this);
|
|
||||||
this.model.contacts.on("change:chat_status", function (contact) {
|
|
||||||
// This might be optimized by instead of first sorting,
|
|
||||||
// finding the correct position in positionContact
|
|
||||||
this.model.contacts.sort();
|
|
||||||
this.positionContact(contact).render();
|
|
||||||
}, this);
|
|
||||||
this.model.contacts.on("destroy", this.onRemove, this);
|
|
||||||
this.model.contacts.on("remove", this.onRemove, this);
|
|
||||||
converse.roster.on('change:groups', this.onContactGroupChange, this);
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function () {
|
|
||||||
this.$el.attr('data-group', this.model.get('name'));
|
|
||||||
this.$el.html(
|
|
||||||
$(converse.templates.group_header({
|
|
||||||
label_group: this.model.get('name'),
|
|
||||||
desc_group_toggle: this.model.get('description'),
|
|
||||||
toggle_state: this.model.get('state')
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
addContact: function (contact) {
|
|
||||||
var view = new converse.RosterContactView({model: contact});
|
|
||||||
this.add(contact.get('id'), view);
|
|
||||||
view = this.positionContact(contact).render();
|
|
||||||
if (contact.showInRoster()) {
|
|
||||||
if (this.model.get('state') === converse.CLOSED) {
|
|
||||||
if (view.$el[0].style.display !== "none") { view.$el.hide(); }
|
|
||||||
if (!this.$el.is(':visible')) { this.$el.show(); }
|
|
||||||
} else {
|
|
||||||
if (this.$el[0].style.display !== "block") { this.show(); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
positionContact: function (contact) {
|
|
||||||
/* Place the contact's DOM element in the correct alphabetical
|
|
||||||
* position amongst the other contacts in this group.
|
|
||||||
*/
|
|
||||||
var view = this.get(contact.get('id'));
|
|
||||||
var index = this.model.contacts.indexOf(contact);
|
|
||||||
view.$el.detach();
|
|
||||||
if (index === 0) {
|
|
||||||
this.$el.after(view.$el);
|
|
||||||
} else if (index === (this.model.contacts.length-1)) {
|
|
||||||
this.$el.nextUntil('dt').last().after(view.$el);
|
|
||||||
} else {
|
|
||||||
this.$el.nextUntil('dt').eq(index).before(view.$el);
|
|
||||||
}
|
|
||||||
return view;
|
|
||||||
},
|
|
||||||
|
|
||||||
show: function () {
|
|
||||||
this.$el.show();
|
|
||||||
_.each(this.getAll(), function (contactView) {
|
|
||||||
if (contactView.model.showInRoster()) {
|
|
||||||
contactView.$el.show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
hide: function () {
|
|
||||||
this.$el.nextUntil('dt').addBack().hide();
|
|
||||||
},
|
|
||||||
|
|
||||||
filter: function (q) {
|
|
||||||
/* Filter the group's contacts based on the query "q".
|
|
||||||
* The query is matched against the contact's full name.
|
|
||||||
* If all contacts are filtered out (i.e. hidden), then the
|
|
||||||
* group must be filtered out as well.
|
|
||||||
*/
|
|
||||||
var matches;
|
|
||||||
if (q.length === 0) {
|
|
||||||
if (this.model.get('state') === converse.OPENED) {
|
|
||||||
this.model.contacts.each(function (item) {
|
|
||||||
if (item.showInRoster()) {
|
|
||||||
this.get(item.get('id')).$el.show();
|
|
||||||
}
|
|
||||||
}.bind(this));
|
|
||||||
}
|
|
||||||
this.showIfNecessary();
|
|
||||||
} else {
|
|
||||||
q = q.toLowerCase();
|
|
||||||
matches = this.model.contacts.filter(utils.contains.not('fullname', q));
|
|
||||||
if (matches.length === this.model.contacts.length) { // hide the whole group
|
|
||||||
this.hide();
|
|
||||||
} else {
|
|
||||||
_.each(matches, function (item) {
|
|
||||||
this.get(item.get('id')).$el.hide();
|
|
||||||
}.bind(this));
|
|
||||||
_.each(this.model.contacts.reject(utils.contains.not('fullname', q)), function (item) {
|
|
||||||
this.get(item.get('id')).$el.show();
|
|
||||||
}.bind(this));
|
|
||||||
this.showIfNecessary();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
showIfNecessary: function () {
|
|
||||||
if (!this.$el.is(':visible') && this.model.contacts.length > 0) {
|
|
||||||
this.$el.show();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
toggle: function (ev) {
|
|
||||||
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
|
||||||
var $el = $(ev.target);
|
|
||||||
if ($el.hasClass("icon-opened")) {
|
|
||||||
this.$el.nextUntil('dt').slideUp();
|
|
||||||
this.model.save({state: converse.CLOSED});
|
|
||||||
$el.removeClass("icon-opened").addClass("icon-closed");
|
|
||||||
} else {
|
|
||||||
$el.removeClass("icon-closed").addClass("icon-opened");
|
|
||||||
this.model.save({state: converse.OPENED});
|
|
||||||
this.filter(
|
|
||||||
converse.rosterview.$('.roster-filter').val(),
|
|
||||||
converse.rosterview.$('.filter-type').val()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onContactGroupChange: function (contact) {
|
|
||||||
var in_this_group = _.contains(contact.get('groups'), this.model.get('name'));
|
|
||||||
var cid = contact.get('id');
|
|
||||||
var in_this_overview = !this.get(cid);
|
|
||||||
if (in_this_group && !in_this_overview) {
|
|
||||||
this.model.contacts.remove(cid);
|
|
||||||
} else if (!in_this_group && in_this_overview) {
|
|
||||||
this.addContact(contact);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onContactSubscriptionChange: function (contact) {
|
|
||||||
if ((this.model.get('name') === HEADER_PENDING_CONTACTS) && contact.get('subscription') !== 'from') {
|
|
||||||
this.model.contacts.remove(contact.get('id'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onContactRequestChange: function (contact) {
|
|
||||||
if ((this.model.get('name') === HEADER_REQUESTING_CONTACTS) && !contact.get('requesting')) {
|
|
||||||
this.model.contacts.remove(contact.get('id'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onRemove: function (contact) {
|
|
||||||
this.remove(contact.get('id'));
|
|
||||||
if (this.model.contacts.length === 0) {
|
|
||||||
this.$el.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
converse.RosterGroups = Backbone.Collection.extend({
|
|
||||||
model: converse.RosterGroup,
|
|
||||||
comparator: function (a, b) {
|
|
||||||
/* Groups are sorted alphabetically, ignoring case.
|
|
||||||
* However, Ungrouped, Requesting Contacts and Pending Contacts
|
|
||||||
* appear last and in that order. */
|
|
||||||
a = a.get('name');
|
|
||||||
b = b.get('name');
|
|
||||||
var special_groups = _.keys(HEADER_WEIGHTS);
|
|
||||||
var a_is_special = _.contains(special_groups, a);
|
|
||||||
var b_is_special = _.contains(special_groups, b);
|
|
||||||
if (!a_is_special && !b_is_special ) {
|
|
||||||
return a.toLowerCase() < b.toLowerCase() ? -1 : (a.toLowerCase() > b.toLowerCase() ? 1 : 0);
|
|
||||||
} else if (a_is_special && b_is_special) {
|
|
||||||
return HEADER_WEIGHTS[a] < HEADER_WEIGHTS[b] ? -1 : (HEADER_WEIGHTS[a] > HEADER_WEIGHTS[b] ? 1 : 0);
|
|
||||||
} else if (!a_is_special && b_is_special) {
|
|
||||||
return (b === HEADER_CURRENT_CONTACTS) ? 1 : -1;
|
|
||||||
} else if (a_is_special && !b_is_special) {
|
|
||||||
return (a === HEADER_CURRENT_CONTACTS) ? -1 : 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
converse.RosterView = Backbone.Overview.extend({
|
|
||||||
tagName: 'div',
|
|
||||||
id: 'converse-roster',
|
|
||||||
events: {
|
|
||||||
"keydown .roster-filter": "liveFilter",
|
|
||||||
"click .onX": "clearFilter",
|
|
||||||
"mousemove .x": "togglePointer",
|
|
||||||
"change .filter-type": "changeFilterType"
|
|
||||||
},
|
|
||||||
|
|
||||||
initialize: function () {
|
|
||||||
this.roster_handler_ref = this.registerRosterHandler();
|
|
||||||
this.rosterx_handler_ref = this.registerRosterXHandler();
|
|
||||||
this.presence_ref = this.registerPresenceHandler();
|
|
||||||
converse.roster.on("add", this.onContactAdd, this);
|
|
||||||
converse.roster.on('change', this.onContactChange, this);
|
|
||||||
converse.roster.on("destroy", this.update, this);
|
|
||||||
converse.roster.on("remove", this.update, this);
|
|
||||||
this.model.on("add", this.onGroupAdd, this);
|
|
||||||
this.model.on("reset", this.reset, this);
|
|
||||||
this.$roster = $('<dl class="roster-contacts" style="display: none;"></dl>');
|
|
||||||
},
|
|
||||||
|
|
||||||
unregisterHandlers: function () {
|
|
||||||
converse.connection.deleteHandler(this.roster_handler_ref);
|
|
||||||
delete this.roster_handler_ref;
|
|
||||||
converse.connection.deleteHandler(this.rosterx_handler_ref);
|
|
||||||
delete this.rosterx_handler_ref;
|
|
||||||
converse.connection.deleteHandler(this.presence_ref);
|
|
||||||
delete this.presence_ref;
|
|
||||||
},
|
|
||||||
|
|
||||||
update: _.debounce(function () {
|
|
||||||
var $count = $('#online-count');
|
|
||||||
$count.text('('+converse.roster.getNumOnlineContacts()+')');
|
|
||||||
if (!$count.is(':visible')) {
|
|
||||||
$count.show();
|
|
||||||
}
|
|
||||||
if (this.$roster.parent().length === 0) {
|
|
||||||
this.$el.append(this.$roster.show());
|
|
||||||
}
|
|
||||||
return this.showHideFilter();
|
|
||||||
}, converse.animate ? 100 : 0),
|
|
||||||
|
|
||||||
render: function () {
|
|
||||||
this.$el.html(converse.templates.roster({
|
|
||||||
placeholder: __('Type to filter'),
|
|
||||||
label_contacts: LABEL_CONTACTS,
|
|
||||||
label_groups: LABEL_GROUPS
|
|
||||||
}));
|
|
||||||
if (!converse.allow_contact_requests) {
|
|
||||||
// XXX: if we ever support live editing of config then
|
|
||||||
// we'll need to be able to remove this class on the fly.
|
|
||||||
this.$el.addClass('no-contact-requests');
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
fetch: function () {
|
|
||||||
this.model.fetch({
|
|
||||||
silent: true, // We use the success handler to handle groups that were added,
|
|
||||||
// we need to first have all groups before positionFetchedGroups
|
|
||||||
// will work properly.
|
|
||||||
success: function (collection, resp, options) {
|
|
||||||
if (collection.length !== 0) {
|
|
||||||
this.positionFetchedGroups(collection, resp, options);
|
|
||||||
}
|
|
||||||
converse.roster.fetch({
|
|
||||||
add: true,
|
|
||||||
success: function (collection) {
|
|
||||||
if (collection.length === 0) {
|
|
||||||
/* We don't have any roster contacts stored in sessionStorage,
|
|
||||||
* so lets fetch the roster from the XMPP server. We pass in
|
|
||||||
* 'sendPresence' as callback method, because after initially
|
|
||||||
* fetching the roster we are ready to receive presence
|
|
||||||
* updates from our contacts.
|
|
||||||
*/
|
|
||||||
converse.roster.fetchFromServer(function () {
|
|
||||||
converse.xmppstatus.sendPresence();
|
|
||||||
});
|
|
||||||
} else if (converse.send_initial_presence) {
|
|
||||||
/* We're not going to fetch the roster again because we have
|
|
||||||
* it already cached in sessionStorage, but we still need to
|
|
||||||
* send out a presence stanza because this is a new session.
|
|
||||||
* See: https://github.com/jcbrand/converse.js/issues/536
|
|
||||||
*/
|
|
||||||
converse.xmppstatus.sendPresence();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}.bind(this)
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
changeFilterType: function (ev) {
|
|
||||||
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
|
||||||
this.clearFilter();
|
|
||||||
this.filter(
|
|
||||||
this.$('.roster-filter').val(),
|
|
||||||
ev.target.value
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
tog: function (v) {
|
|
||||||
return v?'addClass':'removeClass';
|
|
||||||
},
|
|
||||||
|
|
||||||
togglePointer: function (ev) {
|
|
||||||
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
|
||||||
var el = ev.target;
|
|
||||||
$(el)[this.tog(el.offsetWidth-18 < ev.clientX-el.getBoundingClientRect().left)]('onX');
|
|
||||||
},
|
|
||||||
|
|
||||||
filter: function (query, type) {
|
|
||||||
query = query.toLowerCase();
|
|
||||||
if (type === 'groups') {
|
|
||||||
_.each(this.getAll(), function (view, idx) {
|
|
||||||
if (view.model.get('name').toLowerCase().indexOf(query.toLowerCase()) === -1) {
|
|
||||||
view.hide();
|
|
||||||
} else if (view.model.contacts.length > 0) {
|
|
||||||
view.show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
_.each(this.getAll(), function (view) {
|
|
||||||
view.filter(query, type);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
liveFilter: _.debounce(function (ev) {
|
|
||||||
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
|
||||||
var $filter = this.$('.roster-filter');
|
|
||||||
var q = $filter.val();
|
|
||||||
var t = this.$('.filter-type').val();
|
|
||||||
$filter[this.tog(q)]('x');
|
|
||||||
this.filter(q, t);
|
|
||||||
}, 300),
|
|
||||||
|
|
||||||
clearFilter: function (ev) {
|
|
||||||
if (ev && ev.preventDefault) {
|
|
||||||
ev.preventDefault();
|
|
||||||
$(ev.target).removeClass('x onX').val('');
|
|
||||||
}
|
|
||||||
this.filter('');
|
|
||||||
},
|
|
||||||
|
|
||||||
showHideFilter: function () {
|
|
||||||
if (!this.$el.is(':visible')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var $filter = this.$('.roster-filter');
|
|
||||||
var $type = this.$('.filter-type');
|
|
||||||
var visible = $filter.is(':visible');
|
|
||||||
if (visible && $filter.val().length > 0) {
|
|
||||||
// Don't hide if user is currently filtering.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.$roster.hasScrollBar()) {
|
|
||||||
if (!visible) {
|
|
||||||
$filter.show();
|
|
||||||
$type.show();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$filter.hide();
|
|
||||||
$type.hide();
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
reset: function () {
|
|
||||||
converse.roster.reset();
|
|
||||||
this.removeAll();
|
|
||||||
this.$roster = $('<dl class="roster-contacts" style="display: none;"></dl>');
|
|
||||||
this.render().update();
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
registerRosterHandler: function () {
|
|
||||||
converse.connection.addHandler(
|
|
||||||
converse.roster.onRosterPush.bind(converse.roster),
|
|
||||||
Strophe.NS.ROSTER, 'iq', "set"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
registerRosterXHandler: function () {
|
|
||||||
var t = 0;
|
|
||||||
converse.connection.addHandler(
|
|
||||||
function (msg) {
|
|
||||||
window.setTimeout(
|
|
||||||
function () {
|
|
||||||
converse.connection.flush();
|
|
||||||
converse.roster.subscribeToSuggestedItems.bind(converse.roster)(msg);
|
|
||||||
},
|
|
||||||
t
|
|
||||||
);
|
|
||||||
t += $(msg).find('item').length*250;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
Strophe.NS.ROSTERX, 'message', null
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
registerPresenceHandler: function () {
|
|
||||||
converse.connection.addHandler(
|
|
||||||
function (presence) {
|
|
||||||
converse.roster.presenceHandler(presence);
|
|
||||||
return true;
|
|
||||||
}.bind(this), null, 'presence', null);
|
|
||||||
},
|
|
||||||
|
|
||||||
onGroupAdd: function (group) {
|
|
||||||
var view = new converse.RosterGroupView({model: group});
|
|
||||||
this.add(group.get('name'), view.render());
|
|
||||||
this.positionGroup(view);
|
|
||||||
},
|
|
||||||
|
|
||||||
onContactAdd: function (contact) {
|
|
||||||
this.addRosterContact(contact).update();
|
|
||||||
if (!contact.get('vcard_updated')) {
|
|
||||||
// This will update the vcard, which triggers a change
|
|
||||||
// request which will rerender the roster contact.
|
|
||||||
converse.getVCard(contact.get('jid'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onContactChange: function (contact) {
|
|
||||||
this.updateChatBox(contact).update();
|
|
||||||
if (_.has(contact.changed, 'subscription')) {
|
|
||||||
if (contact.changed.subscription === 'from') {
|
|
||||||
this.addContactToGroup(contact, HEADER_PENDING_CONTACTS);
|
|
||||||
} else if (_.contains(['both', 'to'], contact.get('subscription'))) {
|
|
||||||
this.addExistingContact(contact);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (_.has(contact.changed, 'ask') && contact.changed.ask === 'subscribe') {
|
|
||||||
this.addContactToGroup(contact, HEADER_PENDING_CONTACTS);
|
|
||||||
}
|
|
||||||
if (_.has(contact.changed, 'subscription') && contact.changed.requesting === 'true') {
|
|
||||||
this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS);
|
|
||||||
}
|
|
||||||
this.liveFilter();
|
|
||||||
},
|
|
||||||
|
|
||||||
updateChatBox: function (contact) {
|
|
||||||
var chatbox = converse.chatboxes.get(contact.get('jid')),
|
|
||||||
changes = {};
|
|
||||||
if (!chatbox) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
if (_.has(contact.changed, 'chat_status')) {
|
|
||||||
changes.chat_status = contact.get('chat_status');
|
|
||||||
}
|
|
||||||
if (_.has(contact.changed, 'status')) {
|
|
||||||
changes.status = contact.get('status');
|
|
||||||
}
|
|
||||||
chatbox.save(changes);
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
positionFetchedGroups: function (model, resp, options) {
|
|
||||||
/* Instead of throwing an add event for each group
|
|
||||||
* fetched, we wait until they're all fetched and then
|
|
||||||
* we position them.
|
|
||||||
* Works around the problem of positionGroup not
|
|
||||||
* working when all groups besides the one being
|
|
||||||
* positioned aren't already in inserted into the
|
|
||||||
* roster DOM element.
|
|
||||||
*/
|
|
||||||
model.sort();
|
|
||||||
model.each(function (group, idx) {
|
|
||||||
var view = this.get(group.get('name'));
|
|
||||||
if (!view) {
|
|
||||||
view = new converse.RosterGroupView({model: group});
|
|
||||||
this.add(group.get('name'), view.render());
|
|
||||||
}
|
|
||||||
if (idx === 0) {
|
|
||||||
this.$roster.append(view.$el);
|
|
||||||
} else {
|
|
||||||
this.appendGroup(view);
|
|
||||||
}
|
|
||||||
}.bind(this));
|
|
||||||
},
|
|
||||||
|
|
||||||
positionGroup: function (view) {
|
|
||||||
/* Place the group's DOM element in the correct alphabetical
|
|
||||||
* position amongst the other groups in the roster.
|
|
||||||
*/
|
|
||||||
var $groups = this.$roster.find('.roster-group'),
|
|
||||||
index = $groups.length ? this.model.indexOf(view.model) : 0;
|
|
||||||
if (index === 0) {
|
|
||||||
this.$roster.prepend(view.$el);
|
|
||||||
} else if (index === (this.model.length-1)) {
|
|
||||||
this.appendGroup(view);
|
|
||||||
} else {
|
|
||||||
$($groups.eq(index)).before(view.$el);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
appendGroup: function (view) {
|
|
||||||
/* Add the group at the bottom of the roster
|
|
||||||
*/
|
|
||||||
var $last = this.$roster.find('.roster-group').last();
|
|
||||||
var $siblings = $last.siblings('dd');
|
|
||||||
if ($siblings.length > 0) {
|
|
||||||
$siblings.last().after(view.$el);
|
|
||||||
} else {
|
|
||||||
$last.after(view.$el);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
},
|
|
||||||
|
|
||||||
getGroup: function (name) {
|
|
||||||
/* Returns the group as specified by name.
|
|
||||||
* Creates the group if it doesn't exist.
|
|
||||||
*/
|
|
||||||
var view = this.get(name);
|
|
||||||
if (view) {
|
|
||||||
return view.model;
|
|
||||||
}
|
|
||||||
return this.model.create({name: name, id: b64_sha1(name)});
|
|
||||||
},
|
|
||||||
|
|
||||||
addContactToGroup: function (contact, name) {
|
|
||||||
this.getGroup(name).contacts.add(contact);
|
|
||||||
},
|
|
||||||
|
|
||||||
addExistingContact: function (contact) {
|
|
||||||
var groups;
|
|
||||||
if (converse.roster_groups) {
|
|
||||||
groups = contact.get('groups');
|
|
||||||
if (groups.length === 0) {
|
|
||||||
groups = [HEADER_UNGROUPED];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
groups = [HEADER_CURRENT_CONTACTS];
|
|
||||||
}
|
|
||||||
_.each(groups, _.bind(this.addContactToGroup, this, contact));
|
|
||||||
},
|
|
||||||
|
|
||||||
addRosterContact: function (contact) {
|
|
||||||
if (contact.get('subscription') === 'both' || contact.get('subscription') === 'to') {
|
|
||||||
this.addExistingContact(contact);
|
|
||||||
} else {
|
|
||||||
if ((contact.get('ask') === 'subscribe') || (contact.get('subscription') === 'from')) {
|
|
||||||
this.addContactToGroup(contact, HEADER_PENDING_CONTACTS);
|
|
||||||
} else if (contact.get('requesting') === true) {
|
|
||||||
this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
converse.ControlBoxToggle = Backbone.View.extend({
|
converse.ControlBoxToggle = Backbone.View.extend({
|
||||||
tagName: 'a',
|
tagName: 'a',
|
||||||
className: 'toggle-controlbox',
|
className: 'toggle-controlbox',
|
||||||
|
|
|
@ -761,17 +761,18 @@
|
||||||
}.bind(this), 200));
|
}.bind(this), 200));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.afterReconnected = function () {
|
||||||
|
this.chatboxes.registerMessageHandler();
|
||||||
|
this.xmppstatus.sendPresence();
|
||||||
|
this.giveFeedback(__('Contacts'));
|
||||||
|
};
|
||||||
|
|
||||||
this.onReconnected = function () {
|
this.onReconnected = function () {
|
||||||
// We need to re-register all the event handlers on the newly
|
// We need to re-register all the event handlers on the newly
|
||||||
// created connection.
|
// created connection.
|
||||||
var deferred = new $.Deferred();
|
var deferred = new $.Deferred();
|
||||||
this.initStatus(function () {
|
this.initStatus(function () {
|
||||||
// FIXME: leaky abstraction from RosterView
|
this.afterReconnected();
|
||||||
this.rosterview.registerRosterXHandler();
|
|
||||||
this.rosterview.registerPresenceHandler();
|
|
||||||
this.chatboxes.registerMessageHandler();
|
|
||||||
this.xmppstatus.sendPresence();
|
|
||||||
this.giveFeedback(__('Contacts'));
|
|
||||||
deferred.resolve();
|
deferred.resolve();
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
return deferred.promise();
|
return deferred.promise();
|
||||||
|
|
|
@ -36,7 +36,6 @@
|
||||||
notification_icon: '/logo/conversejs.png'
|
notification_icon: '/logo/conversejs.png'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
converse.isOnlyChatStateNotification = function ($msg) {
|
converse.isOnlyChatStateNotification = function ($msg) {
|
||||||
// See XEP-0085 Chat State Notification
|
// See XEP-0085 Chat State Notification
|
||||||
return (
|
return (
|
||||||
|
|
764
src/converse-rosterview.js
Normal file
764
src/converse-rosterview.js
Normal file
|
@ -0,0 +1,764 @@
|
||||||
|
// Converse.js (A browser based XMPP chat client)
|
||||||
|
// http://conversejs.org
|
||||||
|
//
|
||||||
|
// Copyright (c) 2012-2016, Jan-Carel Brand <jc@opkode.com>
|
||||||
|
// Licensed under the Mozilla Public License (MPLv2)
|
||||||
|
//
|
||||||
|
/*global Backbone, define */
|
||||||
|
|
||||||
|
(function (root, factory) {
|
||||||
|
define("converse-rosterview", ["converse-core", "converse-api"], factory);
|
||||||
|
}(this, function (converse, converse_api) {
|
||||||
|
"use strict";
|
||||||
|
var $ = converse_api.env.jQuery,
|
||||||
|
utils = converse_api.env.utils,
|
||||||
|
Strophe = converse_api.env.Strophe,
|
||||||
|
$iq = converse_api.env.$iq,
|
||||||
|
b64_sha1 = converse_api.env.b64_sha1,
|
||||||
|
_ = converse_api.env._,
|
||||||
|
__ = utils.__.bind(converse);
|
||||||
|
|
||||||
|
|
||||||
|
converse_api.plugins.add('rosterview', {
|
||||||
|
|
||||||
|
overrides: {
|
||||||
|
// Overrides mentioned here will be picked up by converse.js's
|
||||||
|
// plugin architecture they will replace existing methods on the
|
||||||
|
// relevant objects or classes.
|
||||||
|
//
|
||||||
|
// New functions which don't exist yet can also be added.
|
||||||
|
|
||||||
|
afterReconnected: function () {
|
||||||
|
this.rosterview.registerRosterXHandler();
|
||||||
|
this.rosterview.registerPresenceHandler();
|
||||||
|
this._super.afterReconnected.apply(this, arguments);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
initialize: function () {
|
||||||
|
/* The initialize function gets called as soon as the plugin is
|
||||||
|
* loaded by converse.js's plugin machinery.
|
||||||
|
*/
|
||||||
|
this.updateSettings({
|
||||||
|
show_toolbar: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
var STATUSES = {
|
||||||
|
'dnd': __('This contact is busy'),
|
||||||
|
'online': __('This contact is online'),
|
||||||
|
'offline': __('This contact is offline'),
|
||||||
|
'unavailable': __('This contact is unavailable'),
|
||||||
|
'xa': __('This contact is away for an extended period'),
|
||||||
|
'away': __('This contact is away')
|
||||||
|
};
|
||||||
|
var DESC_GROUP_TOGGLE = __('Click to hide these contacts');
|
||||||
|
var LABEL_CONTACTS = __('Contacts');
|
||||||
|
var LABEL_GROUPS = __('Groups');
|
||||||
|
var HEADER_CURRENT_CONTACTS = __('My contacts');
|
||||||
|
var HEADER_PENDING_CONTACTS = __('Pending contacts');
|
||||||
|
var HEADER_REQUESTING_CONTACTS = __('Contact requests');
|
||||||
|
var HEADER_UNGROUPED = __('Ungrouped');
|
||||||
|
var HEADER_WEIGHTS = {};
|
||||||
|
HEADER_WEIGHTS[HEADER_CURRENT_CONTACTS] = 0;
|
||||||
|
HEADER_WEIGHTS[HEADER_UNGROUPED] = 1;
|
||||||
|
HEADER_WEIGHTS[HEADER_REQUESTING_CONTACTS] = 2;
|
||||||
|
HEADER_WEIGHTS[HEADER_PENDING_CONTACTS] = 3;
|
||||||
|
|
||||||
|
|
||||||
|
converse.RosterView = Backbone.Overview.extend({
|
||||||
|
tagName: 'div',
|
||||||
|
id: 'converse-roster',
|
||||||
|
events: {
|
||||||
|
"keydown .roster-filter": "liveFilter",
|
||||||
|
"click .onX": "clearFilter",
|
||||||
|
"mousemove .x": "togglePointer",
|
||||||
|
"change .filter-type": "changeFilterType"
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function () {
|
||||||
|
this.roster_handler_ref = this.registerRosterHandler();
|
||||||
|
this.rosterx_handler_ref = this.registerRosterXHandler();
|
||||||
|
this.presence_ref = this.registerPresenceHandler();
|
||||||
|
converse.roster.on("add", this.onContactAdd, this);
|
||||||
|
converse.roster.on('change', this.onContactChange, this);
|
||||||
|
converse.roster.on("destroy", this.update, this);
|
||||||
|
converse.roster.on("remove", this.update, this);
|
||||||
|
this.model.on("add", this.onGroupAdd, this);
|
||||||
|
this.model.on("reset", this.reset, this);
|
||||||
|
this.$roster = $('<dl class="roster-contacts" style="display: none;"></dl>');
|
||||||
|
},
|
||||||
|
|
||||||
|
unregisterHandlers: function () {
|
||||||
|
converse.connection.deleteHandler(this.roster_handler_ref);
|
||||||
|
delete this.roster_handler_ref;
|
||||||
|
converse.connection.deleteHandler(this.rosterx_handler_ref);
|
||||||
|
delete this.rosterx_handler_ref;
|
||||||
|
converse.connection.deleteHandler(this.presence_ref);
|
||||||
|
delete this.presence_ref;
|
||||||
|
},
|
||||||
|
|
||||||
|
update: _.debounce(function () {
|
||||||
|
var $count = $('#online-count');
|
||||||
|
$count.text('('+converse.roster.getNumOnlineContacts()+')');
|
||||||
|
if (!$count.is(':visible')) {
|
||||||
|
$count.show();
|
||||||
|
}
|
||||||
|
if (this.$roster.parent().length === 0) {
|
||||||
|
this.$el.append(this.$roster.show());
|
||||||
|
}
|
||||||
|
return this.showHideFilter();
|
||||||
|
}, converse.animate ? 100 : 0),
|
||||||
|
|
||||||
|
render: function () {
|
||||||
|
this.$el.html(converse.templates.roster({
|
||||||
|
placeholder: __('Type to filter'),
|
||||||
|
label_contacts: LABEL_CONTACTS,
|
||||||
|
label_groups: LABEL_GROUPS
|
||||||
|
}));
|
||||||
|
if (!converse.allow_contact_requests) {
|
||||||
|
// XXX: if we ever support live editing of config then
|
||||||
|
// we'll need to be able to remove this class on the fly.
|
||||||
|
this.$el.addClass('no-contact-requests');
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
fetch: function () {
|
||||||
|
this.model.fetch({
|
||||||
|
silent: true, // We use the success handler to handle groups that were added,
|
||||||
|
// we need to first have all groups before positionFetchedGroups
|
||||||
|
// will work properly.
|
||||||
|
success: function (collection, resp, options) {
|
||||||
|
if (collection.length !== 0) {
|
||||||
|
this.positionFetchedGroups(collection, resp, options);
|
||||||
|
}
|
||||||
|
converse.roster.fetch({
|
||||||
|
add: true,
|
||||||
|
success: function (collection) {
|
||||||
|
if (collection.length === 0) {
|
||||||
|
/* We don't have any roster contacts stored in sessionStorage,
|
||||||
|
* so lets fetch the roster from the XMPP server. We pass in
|
||||||
|
* 'sendPresence' as callback method, because after initially
|
||||||
|
* fetching the roster we are ready to receive presence
|
||||||
|
* updates from our contacts.
|
||||||
|
*/
|
||||||
|
converse.roster.fetchFromServer(function () {
|
||||||
|
converse.xmppstatus.sendPresence();
|
||||||
|
});
|
||||||
|
} else if (converse.send_initial_presence) {
|
||||||
|
/* We're not going to fetch the roster again because we have
|
||||||
|
* it already cached in sessionStorage, but we still need to
|
||||||
|
* send out a presence stanza because this is a new session.
|
||||||
|
* See: https://github.com/jcbrand/converse.js/issues/536
|
||||||
|
*/
|
||||||
|
converse.xmppstatus.sendPresence();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}.bind(this)
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
changeFilterType: function (ev) {
|
||||||
|
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
||||||
|
this.clearFilter();
|
||||||
|
this.filter(
|
||||||
|
this.$('.roster-filter').val(),
|
||||||
|
ev.target.value
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
tog: function (v) {
|
||||||
|
return v?'addClass':'removeClass';
|
||||||
|
},
|
||||||
|
|
||||||
|
togglePointer: function (ev) {
|
||||||
|
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
||||||
|
var el = ev.target;
|
||||||
|
$(el)[this.tog(el.offsetWidth-18 < ev.clientX-el.getBoundingClientRect().left)]('onX');
|
||||||
|
},
|
||||||
|
|
||||||
|
filter: function (query, type) {
|
||||||
|
query = query.toLowerCase();
|
||||||
|
if (type === 'groups') {
|
||||||
|
_.each(this.getAll(), function (view, idx) {
|
||||||
|
if (view.model.get('name').toLowerCase().indexOf(query.toLowerCase()) === -1) {
|
||||||
|
view.hide();
|
||||||
|
} else if (view.model.contacts.length > 0) {
|
||||||
|
view.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_.each(this.getAll(), function (view) {
|
||||||
|
view.filter(query, type);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
liveFilter: _.debounce(function (ev) {
|
||||||
|
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
||||||
|
var $filter = this.$('.roster-filter');
|
||||||
|
var q = $filter.val();
|
||||||
|
var t = this.$('.filter-type').val();
|
||||||
|
$filter[this.tog(q)]('x');
|
||||||
|
this.filter(q, t);
|
||||||
|
}, 300),
|
||||||
|
|
||||||
|
clearFilter: function (ev) {
|
||||||
|
if (ev && ev.preventDefault) {
|
||||||
|
ev.preventDefault();
|
||||||
|
$(ev.target).removeClass('x onX').val('');
|
||||||
|
}
|
||||||
|
this.filter('');
|
||||||
|
},
|
||||||
|
|
||||||
|
showHideFilter: function () {
|
||||||
|
if (!this.$el.is(':visible')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var $filter = this.$('.roster-filter');
|
||||||
|
var $type = this.$('.filter-type');
|
||||||
|
var visible = $filter.is(':visible');
|
||||||
|
if (visible && $filter.val().length > 0) {
|
||||||
|
// Don't hide if user is currently filtering.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.$roster.hasScrollBar()) {
|
||||||
|
if (!visible) {
|
||||||
|
$filter.show();
|
||||||
|
$type.show();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$filter.hide();
|
||||||
|
$type.hide();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
reset: function () {
|
||||||
|
converse.roster.reset();
|
||||||
|
this.removeAll();
|
||||||
|
this.$roster = $('<dl class="roster-contacts" style="display: none;"></dl>');
|
||||||
|
this.render().update();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
registerRosterHandler: function () {
|
||||||
|
converse.connection.addHandler(
|
||||||
|
converse.roster.onRosterPush.bind(converse.roster),
|
||||||
|
Strophe.NS.ROSTER, 'iq', "set"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
registerRosterXHandler: function () {
|
||||||
|
var t = 0;
|
||||||
|
converse.connection.addHandler(
|
||||||
|
function (msg) {
|
||||||
|
window.setTimeout(
|
||||||
|
function () {
|
||||||
|
converse.connection.flush();
|
||||||
|
converse.roster.subscribeToSuggestedItems.bind(converse.roster)(msg);
|
||||||
|
},
|
||||||
|
t
|
||||||
|
);
|
||||||
|
t += $(msg).find('item').length*250;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
Strophe.NS.ROSTERX, 'message', null
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
registerPresenceHandler: function () {
|
||||||
|
converse.connection.addHandler(
|
||||||
|
function (presence) {
|
||||||
|
converse.roster.presenceHandler(presence);
|
||||||
|
return true;
|
||||||
|
}.bind(this), null, 'presence', null);
|
||||||
|
},
|
||||||
|
|
||||||
|
onGroupAdd: function (group) {
|
||||||
|
var view = new converse.RosterGroupView({model: group});
|
||||||
|
this.add(group.get('name'), view.render());
|
||||||
|
this.positionGroup(view);
|
||||||
|
},
|
||||||
|
|
||||||
|
onContactAdd: function (contact) {
|
||||||
|
this.addRosterContact(contact).update();
|
||||||
|
if (!contact.get('vcard_updated')) {
|
||||||
|
// This will update the vcard, which triggers a change
|
||||||
|
// request which will rerender the roster contact.
|
||||||
|
converse.getVCard(contact.get('jid'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onContactChange: function (contact) {
|
||||||
|
this.updateChatBox(contact).update();
|
||||||
|
if (_.has(contact.changed, 'subscription')) {
|
||||||
|
if (contact.changed.subscription === 'from') {
|
||||||
|
this.addContactToGroup(contact, HEADER_PENDING_CONTACTS);
|
||||||
|
} else if (_.contains(['both', 'to'], contact.get('subscription'))) {
|
||||||
|
this.addExistingContact(contact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_.has(contact.changed, 'ask') && contact.changed.ask === 'subscribe') {
|
||||||
|
this.addContactToGroup(contact, HEADER_PENDING_CONTACTS);
|
||||||
|
}
|
||||||
|
if (_.has(contact.changed, 'subscription') && contact.changed.requesting === 'true') {
|
||||||
|
this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS);
|
||||||
|
}
|
||||||
|
this.liveFilter();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateChatBox: function (contact) {
|
||||||
|
var chatbox = converse.chatboxes.get(contact.get('jid')),
|
||||||
|
changes = {};
|
||||||
|
if (!chatbox) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
if (_.has(contact.changed, 'chat_status')) {
|
||||||
|
changes.chat_status = contact.get('chat_status');
|
||||||
|
}
|
||||||
|
if (_.has(contact.changed, 'status')) {
|
||||||
|
changes.status = contact.get('status');
|
||||||
|
}
|
||||||
|
chatbox.save(changes);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
positionFetchedGroups: function (model, resp, options) {
|
||||||
|
/* Instead of throwing an add event for each group
|
||||||
|
* fetched, we wait until they're all fetched and then
|
||||||
|
* we position them.
|
||||||
|
* Works around the problem of positionGroup not
|
||||||
|
* working when all groups besides the one being
|
||||||
|
* positioned aren't already in inserted into the
|
||||||
|
* roster DOM element.
|
||||||
|
*/
|
||||||
|
model.sort();
|
||||||
|
model.each(function (group, idx) {
|
||||||
|
var view = this.get(group.get('name'));
|
||||||
|
if (!view) {
|
||||||
|
view = new converse.RosterGroupView({model: group});
|
||||||
|
this.add(group.get('name'), view.render());
|
||||||
|
}
|
||||||
|
if (idx === 0) {
|
||||||
|
this.$roster.append(view.$el);
|
||||||
|
} else {
|
||||||
|
this.appendGroup(view);
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
positionGroup: function (view) {
|
||||||
|
/* Place the group's DOM element in the correct alphabetical
|
||||||
|
* position amongst the other groups in the roster.
|
||||||
|
*/
|
||||||
|
var $groups = this.$roster.find('.roster-group'),
|
||||||
|
index = $groups.length ? this.model.indexOf(view.model) : 0;
|
||||||
|
if (index === 0) {
|
||||||
|
this.$roster.prepend(view.$el);
|
||||||
|
} else if (index === (this.model.length-1)) {
|
||||||
|
this.appendGroup(view);
|
||||||
|
} else {
|
||||||
|
$($groups.eq(index)).before(view.$el);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
appendGroup: function (view) {
|
||||||
|
/* Add the group at the bottom of the roster
|
||||||
|
*/
|
||||||
|
var $last = this.$roster.find('.roster-group').last();
|
||||||
|
var $siblings = $last.siblings('dd');
|
||||||
|
if ($siblings.length > 0) {
|
||||||
|
$siblings.last().after(view.$el);
|
||||||
|
} else {
|
||||||
|
$last.after(view.$el);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
getGroup: function (name) {
|
||||||
|
/* Returns the group as specified by name.
|
||||||
|
* Creates the group if it doesn't exist.
|
||||||
|
*/
|
||||||
|
var view = this.get(name);
|
||||||
|
if (view) {
|
||||||
|
return view.model;
|
||||||
|
}
|
||||||
|
return this.model.create({name: name, id: b64_sha1(name)});
|
||||||
|
},
|
||||||
|
|
||||||
|
addContactToGroup: function (contact, name) {
|
||||||
|
this.getGroup(name).contacts.add(contact);
|
||||||
|
},
|
||||||
|
|
||||||
|
addExistingContact: function (contact) {
|
||||||
|
var groups;
|
||||||
|
if (converse.roster_groups) {
|
||||||
|
groups = contact.get('groups');
|
||||||
|
if (groups.length === 0) {
|
||||||
|
groups = [HEADER_UNGROUPED];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
groups = [HEADER_CURRENT_CONTACTS];
|
||||||
|
}
|
||||||
|
_.each(groups, _.bind(this.addContactToGroup, this, contact));
|
||||||
|
},
|
||||||
|
|
||||||
|
addRosterContact: function (contact) {
|
||||||
|
if (contact.get('subscription') === 'both' || contact.get('subscription') === 'to') {
|
||||||
|
this.addExistingContact(contact);
|
||||||
|
} else {
|
||||||
|
if ((contact.get('ask') === 'subscribe') || (contact.get('subscription') === 'from')) {
|
||||||
|
this.addContactToGroup(contact, HEADER_PENDING_CONTACTS);
|
||||||
|
} else if (contact.get('requesting') === true) {
|
||||||
|
this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
converse.RosterContactView = Backbone.View.extend({
|
||||||
|
tagName: 'dd',
|
||||||
|
|
||||||
|
events: {
|
||||||
|
"click .accept-xmpp-request": "acceptRequest",
|
||||||
|
"click .decline-xmpp-request": "declineRequest",
|
||||||
|
"click .open-chat": "openChat",
|
||||||
|
"click .remove-xmpp-contact": "removeContact"
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function () {
|
||||||
|
this.model.on("change", this.render, this);
|
||||||
|
this.model.on("remove", this.remove, this);
|
||||||
|
this.model.on("destroy", this.remove, this);
|
||||||
|
this.model.on("open", this.openChat, this);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function () {
|
||||||
|
if (!this.model.showInRoster()) {
|
||||||
|
this.$el.hide();
|
||||||
|
return this;
|
||||||
|
} else if (this.$el[0].style.display === "none") {
|
||||||
|
this.$el.show();
|
||||||
|
}
|
||||||
|
var item = this.model,
|
||||||
|
ask = item.get('ask'),
|
||||||
|
chat_status = item.get('chat_status'),
|
||||||
|
requesting = item.get('requesting'),
|
||||||
|
subscription = item.get('subscription');
|
||||||
|
|
||||||
|
var classes_to_remove = [
|
||||||
|
'current-xmpp-contact',
|
||||||
|
'pending-xmpp-contact',
|
||||||
|
'requesting-xmpp-contact'
|
||||||
|
].concat(_.keys(STATUSES));
|
||||||
|
|
||||||
|
_.each(classes_to_remove,
|
||||||
|
function (cls) {
|
||||||
|
if (this.el.className.indexOf(cls) !== -1) {
|
||||||
|
this.$el.removeClass(cls);
|
||||||
|
}
|
||||||
|
}, this);
|
||||||
|
this.$el.addClass(chat_status).data('status', chat_status);
|
||||||
|
|
||||||
|
if ((ask === 'subscribe') || (subscription === 'from')) {
|
||||||
|
/* ask === 'subscribe'
|
||||||
|
* Means we have asked to subscribe to them.
|
||||||
|
*
|
||||||
|
* subscription === 'from'
|
||||||
|
* They are subscribed to use, but not vice versa.
|
||||||
|
* We assume that there is a pending subscription
|
||||||
|
* from us to them (otherwise we're in a state not
|
||||||
|
* supported by converse.js).
|
||||||
|
*
|
||||||
|
* So in both cases the user is a "pending" contact.
|
||||||
|
*/
|
||||||
|
this.$el.addClass('pending-xmpp-contact');
|
||||||
|
this.$el.html(converse.templates.pending_contact(
|
||||||
|
_.extend(item.toJSON(), {
|
||||||
|
'desc_remove': __('Click to remove this contact'),
|
||||||
|
'allow_chat_pending_contacts': converse.allow_chat_pending_contacts
|
||||||
|
})
|
||||||
|
));
|
||||||
|
} else if (requesting === true) {
|
||||||
|
this.$el.addClass('requesting-xmpp-contact');
|
||||||
|
this.$el.html(converse.templates.requesting_contact(
|
||||||
|
_.extend(item.toJSON(), {
|
||||||
|
'desc_accept': __("Click to accept this contact request"),
|
||||||
|
'desc_decline': __("Click to decline this contact request"),
|
||||||
|
'allow_chat_pending_contacts': converse.allow_chat_pending_contacts
|
||||||
|
})
|
||||||
|
));
|
||||||
|
converse.controlboxtoggle.showControlBox();
|
||||||
|
} else if (subscription === 'both' || subscription === 'to') {
|
||||||
|
this.$el.addClass('current-xmpp-contact');
|
||||||
|
this.$el.removeClass(_.without(['both', 'to'], subscription)[0]).addClass(subscription);
|
||||||
|
this.$el.html(converse.templates.roster_item(
|
||||||
|
_.extend(item.toJSON(), {
|
||||||
|
'desc_status': STATUSES[chat_status||'offline'],
|
||||||
|
'desc_chat': __('Click to chat with this contact'),
|
||||||
|
'desc_remove': __('Click to remove this contact'),
|
||||||
|
'title_fullname': __('Name'),
|
||||||
|
'allow_contact_removal': converse.allow_contact_removal
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
openChat: function (ev) {
|
||||||
|
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
||||||
|
return converse.chatboxviews.showChat(this.model.attributes);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeContact: function (ev) {
|
||||||
|
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
||||||
|
if (!converse.allow_contact_removal) { return; }
|
||||||
|
var result = confirm(__("Are you sure you want to remove this contact?"));
|
||||||
|
if (result === true) {
|
||||||
|
var iq = $iq({type: 'set'})
|
||||||
|
.c('query', {xmlns: Strophe.NS.ROSTER})
|
||||||
|
.c('item', {jid: this.model.get('jid'), subscription: "remove"});
|
||||||
|
converse.connection.sendIQ(iq,
|
||||||
|
function (iq) {
|
||||||
|
this.model.destroy();
|
||||||
|
this.remove();
|
||||||
|
}.bind(this),
|
||||||
|
function (err) {
|
||||||
|
alert(__("Sorry, there was an error while trying to remove "+name+" as a contact."));
|
||||||
|
converse.log(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
acceptRequest: function (ev) {
|
||||||
|
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
||||||
|
converse.roster.sendContactAddIQ(
|
||||||
|
this.model.get('jid'),
|
||||||
|
this.model.get('fullname'),
|
||||||
|
[],
|
||||||
|
function () { this.model.authorize().subscribe(); }.bind(this)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
declineRequest: function (ev) {
|
||||||
|
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
||||||
|
var result = confirm(__("Are you sure you want to decline this contact request?"));
|
||||||
|
if (result === true) {
|
||||||
|
this.model.unauthorize().destroy();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
converse.RosterGroup = Backbone.Model.extend({
|
||||||
|
initialize: function (attributes, options) {
|
||||||
|
this.set(_.extend({
|
||||||
|
description: DESC_GROUP_TOGGLE,
|
||||||
|
state: converse.OPENED
|
||||||
|
}, attributes));
|
||||||
|
// Collection of contacts belonging to this group.
|
||||||
|
this.contacts = new converse.RosterContacts();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
converse.RosterGroupView = Backbone.Overview.extend({
|
||||||
|
tagName: 'dt',
|
||||||
|
className: 'roster-group',
|
||||||
|
events: {
|
||||||
|
"click a.group-toggle": "toggle"
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function () {
|
||||||
|
this.model.contacts.on("add", this.addContact, this);
|
||||||
|
this.model.contacts.on("change:subscription", this.onContactSubscriptionChange, this);
|
||||||
|
this.model.contacts.on("change:requesting", this.onContactRequestChange, this);
|
||||||
|
this.model.contacts.on("change:chat_status", function (contact) {
|
||||||
|
// This might be optimized by instead of first sorting,
|
||||||
|
// finding the correct position in positionContact
|
||||||
|
this.model.contacts.sort();
|
||||||
|
this.positionContact(contact).render();
|
||||||
|
}, this);
|
||||||
|
this.model.contacts.on("destroy", this.onRemove, this);
|
||||||
|
this.model.contacts.on("remove", this.onRemove, this);
|
||||||
|
converse.roster.on('change:groups', this.onContactGroupChange, this);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function () {
|
||||||
|
this.$el.attr('data-group', this.model.get('name'));
|
||||||
|
this.$el.html(
|
||||||
|
$(converse.templates.group_header({
|
||||||
|
label_group: this.model.get('name'),
|
||||||
|
desc_group_toggle: this.model.get('description'),
|
||||||
|
toggle_state: this.model.get('state')
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
addContact: function (contact) {
|
||||||
|
var view = new converse.RosterContactView({model: contact});
|
||||||
|
this.add(contact.get('id'), view);
|
||||||
|
view = this.positionContact(contact).render();
|
||||||
|
if (contact.showInRoster()) {
|
||||||
|
if (this.model.get('state') === converse.CLOSED) {
|
||||||
|
if (view.$el[0].style.display !== "none") { view.$el.hide(); }
|
||||||
|
if (!this.$el.is(':visible')) { this.$el.show(); }
|
||||||
|
} else {
|
||||||
|
if (this.$el[0].style.display !== "block") { this.show(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
positionContact: function (contact) {
|
||||||
|
/* Place the contact's DOM element in the correct alphabetical
|
||||||
|
* position amongst the other contacts in this group.
|
||||||
|
*/
|
||||||
|
var view = this.get(contact.get('id'));
|
||||||
|
var index = this.model.contacts.indexOf(contact);
|
||||||
|
view.$el.detach();
|
||||||
|
if (index === 0) {
|
||||||
|
this.$el.after(view.$el);
|
||||||
|
} else if (index === (this.model.contacts.length-1)) {
|
||||||
|
this.$el.nextUntil('dt').last().after(view.$el);
|
||||||
|
} else {
|
||||||
|
this.$el.nextUntil('dt').eq(index).before(view.$el);
|
||||||
|
}
|
||||||
|
return view;
|
||||||
|
},
|
||||||
|
|
||||||
|
show: function () {
|
||||||
|
this.$el.show();
|
||||||
|
_.each(this.getAll(), function (contactView) {
|
||||||
|
if (contactView.model.showInRoster()) {
|
||||||
|
contactView.$el.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
hide: function () {
|
||||||
|
this.$el.nextUntil('dt').addBack().hide();
|
||||||
|
},
|
||||||
|
|
||||||
|
filter: function (q) {
|
||||||
|
/* Filter the group's contacts based on the query "q".
|
||||||
|
* The query is matched against the contact's full name.
|
||||||
|
* If all contacts are filtered out (i.e. hidden), then the
|
||||||
|
* group must be filtered out as well.
|
||||||
|
*/
|
||||||
|
var matches;
|
||||||
|
if (q.length === 0) {
|
||||||
|
if (this.model.get('state') === converse.OPENED) {
|
||||||
|
this.model.contacts.each(function (item) {
|
||||||
|
if (item.showInRoster()) {
|
||||||
|
this.get(item.get('id')).$el.show();
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
this.showIfNecessary();
|
||||||
|
} else {
|
||||||
|
q = q.toLowerCase();
|
||||||
|
matches = this.model.contacts.filter(utils.contains.not('fullname', q));
|
||||||
|
if (matches.length === this.model.contacts.length) { // hide the whole group
|
||||||
|
this.hide();
|
||||||
|
} else {
|
||||||
|
_.each(matches, function (item) {
|
||||||
|
this.get(item.get('id')).$el.hide();
|
||||||
|
}.bind(this));
|
||||||
|
_.each(this.model.contacts.reject(utils.contains.not('fullname', q)), function (item) {
|
||||||
|
this.get(item.get('id')).$el.show();
|
||||||
|
}.bind(this));
|
||||||
|
this.showIfNecessary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showIfNecessary: function () {
|
||||||
|
if (!this.$el.is(':visible') && this.model.contacts.length > 0) {
|
||||||
|
this.$el.show();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggle: function (ev) {
|
||||||
|
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
||||||
|
var $el = $(ev.target);
|
||||||
|
if ($el.hasClass("icon-opened")) {
|
||||||
|
this.$el.nextUntil('dt').slideUp();
|
||||||
|
this.model.save({state: converse.CLOSED});
|
||||||
|
$el.removeClass("icon-opened").addClass("icon-closed");
|
||||||
|
} else {
|
||||||
|
$el.removeClass("icon-closed").addClass("icon-opened");
|
||||||
|
this.model.save({state: converse.OPENED});
|
||||||
|
this.filter(
|
||||||
|
converse.rosterview.$('.roster-filter').val(),
|
||||||
|
converse.rosterview.$('.filter-type').val()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onContactGroupChange: function (contact) {
|
||||||
|
var in_this_group = _.contains(contact.get('groups'), this.model.get('name'));
|
||||||
|
var cid = contact.get('id');
|
||||||
|
var in_this_overview = !this.get(cid);
|
||||||
|
if (in_this_group && !in_this_overview) {
|
||||||
|
this.model.contacts.remove(cid);
|
||||||
|
} else if (!in_this_group && in_this_overview) {
|
||||||
|
this.addContact(contact);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onContactSubscriptionChange: function (contact) {
|
||||||
|
if ((this.model.get('name') === HEADER_PENDING_CONTACTS) && contact.get('subscription') !== 'from') {
|
||||||
|
this.model.contacts.remove(contact.get('id'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onContactRequestChange: function (contact) {
|
||||||
|
if ((this.model.get('name') === HEADER_REQUESTING_CONTACTS) && !contact.get('requesting')) {
|
||||||
|
this.model.contacts.remove(contact.get('id'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onRemove: function (contact) {
|
||||||
|
this.remove(contact.get('id'));
|
||||||
|
if (this.model.contacts.length === 0) {
|
||||||
|
this.$el.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
converse.RosterGroups = Backbone.Collection.extend({
|
||||||
|
model: converse.RosterGroup,
|
||||||
|
comparator: function (a, b) {
|
||||||
|
/* Groups are sorted alphabetically, ignoring case.
|
||||||
|
* However, Ungrouped, Requesting Contacts and Pending Contacts
|
||||||
|
* appear last and in that order. */
|
||||||
|
a = a.get('name');
|
||||||
|
b = b.get('name');
|
||||||
|
var special_groups = _.keys(HEADER_WEIGHTS);
|
||||||
|
var a_is_special = _.contains(special_groups, a);
|
||||||
|
var b_is_special = _.contains(special_groups, b);
|
||||||
|
if (!a_is_special && !b_is_special ) {
|
||||||
|
return a.toLowerCase() < b.toLowerCase() ? -1 : (a.toLowerCase() > b.toLowerCase() ? 1 : 0);
|
||||||
|
} else if (a_is_special && b_is_special) {
|
||||||
|
return HEADER_WEIGHTS[a] < HEADER_WEIGHTS[b] ? -1 : (HEADER_WEIGHTS[a] > HEADER_WEIGHTS[b] ? 1 : 0);
|
||||||
|
} else if (!a_is_special && b_is_special) {
|
||||||
|
return (b === HEADER_CURRENT_CONTACTS) ? 1 : -1;
|
||||||
|
} else if (a_is_special && !b_is_special) {
|
||||||
|
return (a === HEADER_CURRENT_CONTACTS) ? -1 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}));
|
Loading…
Reference in New Issue
Block a user