Add the ability to filter contacts by chat state.
The roster filter is now also remembered across page loads.
This commit is contained in:
parent
885c553e2e
commit
8e0f8f0a6d
|
@ -1848,6 +1848,15 @@
|
||||||
background-position: right 3px center; }
|
background-position: right 3px center; }
|
||||||
#conversejs #converse-roster .roster-filter-group .roster-filter.onX {
|
#conversejs #converse-roster .roster-filter-group .roster-filter.onX {
|
||||||
cursor: pointer; }
|
cursor: pointer; }
|
||||||
|
#conversejs #converse-roster .roster-filter-group .state-type {
|
||||||
|
float: left;
|
||||||
|
border: 1px solid #999;
|
||||||
|
font-size: calc(14px - 2px);
|
||||||
|
height: 25px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
padding-left: 0.4em;
|
||||||
|
width: 53%; }
|
||||||
#conversejs #converse-roster .roster-filter-group .filter-type {
|
#conversejs #converse-roster .roster-filter-group .filter-type {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
float: right;
|
float: right;
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
|
|
||||||
## 1.0.0 (Unreleased)
|
## 1.0.0 (Unreleased)
|
||||||
|
|
||||||
- Better Sass/CSS for responsive/mobile views. [jcbrand]
|
|
||||||
- Split converse.js up into different plugin modules. [jcbrand]
|
- Split converse.js up into different plugin modules. [jcbrand]
|
||||||
|
- Better Sass/CSS for responsive/mobile views. New mobile-only build. [jcbrand]
|
||||||
|
- Roster contacts can now be filtered by chat state and roster filters are
|
||||||
|
remembered across page loads. [jcbrand]
|
||||||
- Add support for messages with type `headline`, often used for notifications
|
- Add support for messages with type `headline`, often used for notifications
|
||||||
from the server. [jcbrand]
|
from the server. [jcbrand]
|
||||||
- Add stanza-specific event listener `converse.listen.stanza`.
|
- Add stanza-specific event listener `converse.listen.stanza`.
|
||||||
|
|
|
@ -44,6 +44,16 @@
|
||||||
.roster-filter.onX {
|
.roster-filter.onX {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.state-type {
|
||||||
|
float: left;
|
||||||
|
border: 1px solid #999;
|
||||||
|
font-size: calc(#{$font-size} - 2px);
|
||||||
|
height: $controlbox-dropdown-height;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
padding-left: 0.4em;
|
||||||
|
width: 53%;
|
||||||
|
}
|
||||||
.filter-type {
|
.filter-type {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
float: right;
|
float: right;
|
||||||
|
|
|
@ -138,28 +138,38 @@
|
||||||
test_utils.openControlBox();
|
test_utils.openControlBox();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("will only appear when roster contacts flow over the visible area", $.proxy(function () {
|
it("will only appear when roster contacts flow over the visible area", function () {
|
||||||
_clearContacts();
|
|
||||||
var $filter = converse.rosterview.$('.roster-filter');
|
var $filter = converse.rosterview.$('.roster-filter');
|
||||||
var names = mock.cur_names;
|
var names = mock.cur_names;
|
||||||
expect($filter.length).toBe(1);
|
runs(function () {
|
||||||
expect($filter.is(':visible')).toBeFalsy();
|
_clearContacts();
|
||||||
for (var i=0; i<names.length; i++) {
|
|
||||||
converse.roster.create({
|
|
||||||
ask: null,
|
|
||||||
fullname: names[i],
|
|
||||||
jid: names[i].replace(/ /g,'.').toLowerCase() + '@localhost',
|
|
||||||
requesting: 'false',
|
|
||||||
subscription: 'both'
|
|
||||||
});
|
|
||||||
converse.rosterview.update(); // XXX: Will normally called as event handler
|
converse.rosterview.update(); // XXX: Will normally called as event handler
|
||||||
|
});
|
||||||
|
waits(5); // Needed, due to debounce
|
||||||
|
runs(function () {
|
||||||
|
expect($filter.length).toBe(1);
|
||||||
|
expect($filter.is(':visible')).toBeFalsy();
|
||||||
|
for (var i=0; i<names.length; i++) {
|
||||||
|
converse.roster.create({
|
||||||
|
ask: null,
|
||||||
|
fullname: names[i],
|
||||||
|
jid: names[i].replace(/ /g,'.').toLowerCase() + '@localhost',
|
||||||
|
requesting: 'false',
|
||||||
|
subscription: 'both'
|
||||||
|
});
|
||||||
|
converse.rosterview.update(); // XXX: Will normally called as event handler
|
||||||
|
}
|
||||||
|
});
|
||||||
|
waits(5); // Needed, due to debounce
|
||||||
|
runs(function () {
|
||||||
|
$filter = converse.rosterview.$('.roster-filter');
|
||||||
if (converse.rosterview.$roster.hasScrollBar()) {
|
if (converse.rosterview.$roster.hasScrollBar()) {
|
||||||
expect($filter.is(':visible')).toBeTruthy();
|
expect($filter.is(':visible')).toBeTruthy();
|
||||||
} else {
|
} else {
|
||||||
expect($filter.is(':visible')).toBeFalsy();
|
expect($filter.is(':visible')).toBeFalsy();
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}, converse));
|
});
|
||||||
|
|
||||||
it("can be used to filter the contacts shown", function () {
|
it("can be used to filter the contacts shown", function () {
|
||||||
var $filter;
|
var $filter;
|
||||||
|
@ -171,8 +181,9 @@
|
||||||
$filter = converse.rosterview.$('.roster-filter');
|
$filter = converse.rosterview.$('.roster-filter');
|
||||||
$roster = converse.rosterview.$roster;
|
$roster = converse.rosterview.$roster;
|
||||||
});
|
});
|
||||||
waits(350); // Needed, due to debounce
|
waits(5); // Needed, due to debounce in "update" method
|
||||||
runs(function () {
|
runs(function () {
|
||||||
|
converse.rosterview.filter_view.delegateEvents();
|
||||||
expect($roster.find('dd:visible').length).toBe(15);
|
expect($roster.find('dd:visible').length).toBe(15);
|
||||||
expect($roster.find('dt:visible').length).toBe(5);
|
expect($roster.find('dt:visible').length).toBe(5);
|
||||||
$filter.val("candice");
|
$filter.val("candice");
|
||||||
|
@ -180,30 +191,33 @@
|
||||||
expect($roster.find('dt:visible').length).toBe(5); // ditto
|
expect($roster.find('dt:visible').length).toBe(5); // ditto
|
||||||
$filter.trigger('keydown');
|
$filter.trigger('keydown');
|
||||||
});
|
});
|
||||||
waits(350); // Needed, due to debounce
|
waits(550); // Needed, due to debounce
|
||||||
runs (function () {
|
runs (function () {
|
||||||
expect($roster.find('dd:visible').length).toBe(1);
|
expect($roster.find('dd:visible').length).toBe(1);
|
||||||
expect($roster.find('dd:visible').eq(0).text().trim()).toBe('Candice van der Knijff');
|
expect($roster.find('dd:visible').eq(0).text().trim()).toBe('Candice van der Knijff');
|
||||||
expect($roster.find('dt:visible').length).toBe(1);
|
expect($roster.find('dt:visible').length).toBe(1);
|
||||||
expect($roster.find('dt:visible').eq(0).text()).toBe('colleagues');
|
expect($roster.find('dt:visible').eq(0).text()).toBe('colleagues');
|
||||||
|
$filter = converse.rosterview.$('.roster-filter');
|
||||||
$filter.val("an");
|
$filter.val("an");
|
||||||
$filter.trigger('keydown');
|
$filter.trigger('keydown');
|
||||||
});
|
});
|
||||||
waits(350); // Needed, due to debounce
|
waits(550); // Needed, due to debounce
|
||||||
runs (function () {
|
runs (function () {
|
||||||
expect($roster.find('dd:visible').length).toBe(5);
|
expect($roster.find('dd:visible').length).toBe(5);
|
||||||
expect($roster.find('dt:visible').length).toBe(4);
|
expect($roster.find('dt:visible').length).toBe(4);
|
||||||
|
$filter = converse.rosterview.$('.roster-filter');
|
||||||
$filter.val("xxx");
|
$filter.val("xxx");
|
||||||
$filter.trigger('keydown');
|
$filter.trigger('keydown');
|
||||||
});
|
});
|
||||||
waits(350); // Needed, due to debounce
|
waits(550); // Needed, due to debounce
|
||||||
runs (function () {
|
runs (function () {
|
||||||
expect($roster.find('dd:visible').length).toBe(0);
|
expect($roster.find('dd:visible').length).toBe(0);
|
||||||
expect($roster.find('dt:visible').length).toBe(0);
|
expect($roster.find('dt:visible').length).toBe(0);
|
||||||
|
$filter = converse.rosterview.$('.roster-filter');
|
||||||
$filter.val(""); // Check that contacts are shown again, when the filter string is cleared.
|
$filter.val(""); // Check that contacts are shown again, when the filter string is cleared.
|
||||||
$filter.trigger('keydown');
|
$filter.trigger('keydown');
|
||||||
});
|
});
|
||||||
waits(350); // Needed, due to debounce
|
waits(550); // Needed, due to debounce
|
||||||
runs(function () {
|
runs(function () {
|
||||||
expect($roster.find('dd:visible').length).toBe(15);
|
expect($roster.find('dd:visible').length).toBe(15);
|
||||||
expect($roster.find('dt:visible').length).toBe(5);
|
expect($roster.find('dt:visible').length).toBe(5);
|
||||||
|
@ -219,12 +233,13 @@
|
||||||
converse.roster_groups = true;
|
converse.roster_groups = true;
|
||||||
_clearContacts();
|
_clearContacts();
|
||||||
utils.createGroupedContacts();
|
utils.createGroupedContacts();
|
||||||
|
converse.rosterview.filter_view.delegateEvents();
|
||||||
$filter = converse.rosterview.$('.roster-filter');
|
$filter = converse.rosterview.$('.roster-filter');
|
||||||
$roster = converse.rosterview.$roster;
|
$roster = converse.rosterview.$roster;
|
||||||
$type = converse.rosterview.$('.filter-type');
|
$type = converse.rosterview.$('.filter-type');
|
||||||
$type.val('groups');
|
$type.val('groups');
|
||||||
});
|
});
|
||||||
waits(350); // Needed, due to debounce
|
waits(550); // Needed, due to debounce
|
||||||
runs(function () {
|
runs(function () {
|
||||||
expect($roster.find('dd:visible').length).toBe(15);
|
expect($roster.find('dd:visible').length).toBe(15);
|
||||||
expect($roster.find('dt:visible').length).toBe(5);
|
expect($roster.find('dt:visible').length).toBe(5);
|
||||||
|
@ -233,22 +248,24 @@
|
||||||
expect($roster.find('dt:visible').length).toBe(5); // ditto
|
expect($roster.find('dt:visible').length).toBe(5); // ditto
|
||||||
$filter.trigger('keydown');
|
$filter.trigger('keydown');
|
||||||
});
|
});
|
||||||
waits(350); // Needed, due to debounce
|
waits(550); // Needed, due to debounce
|
||||||
runs (function () {
|
runs (function () {
|
||||||
expect($roster.find('dt:visible').length).toBe(1);
|
expect($roster.find('dt:visible').length).toBe(1);
|
||||||
expect($roster.find('dt:visible').eq(0).text()).toBe('colleagues');
|
expect($roster.find('dt:visible').eq(0).text()).toBe('colleagues');
|
||||||
// Check that all contacts under the group are shown
|
// Check that all contacts under the group are shown
|
||||||
expect($roster.find('dt:visible').nextUntil('dt', 'dd:hidden').length).toBe(0);
|
expect($roster.find('dt:visible').nextUntil('dt', 'dd:hidden').length).toBe(0);
|
||||||
|
$filter = converse.rosterview.$('.roster-filter');
|
||||||
$filter.val("xxx");
|
$filter.val("xxx");
|
||||||
$filter.trigger('keydown');
|
$filter.trigger('keydown');
|
||||||
});
|
});
|
||||||
waits(350); // Needed, due to debounce
|
waits(550); // Needed, due to debounce
|
||||||
runs (function () {
|
runs (function () {
|
||||||
expect($roster.find('dt:visible').length).toBe(0);
|
expect($roster.find('dt:visible').length).toBe(0);
|
||||||
|
$filter = converse.rosterview.$('.roster-filter');
|
||||||
$filter.val(""); // Check that groups are shown again, when the filter string is cleared.
|
$filter.val(""); // Check that groups are shown again, when the filter string is cleared.
|
||||||
$filter.trigger('keydown');
|
$filter.trigger('keydown');
|
||||||
});
|
});
|
||||||
waits(350); // Needed, due to debounce
|
waits(550); // Needed, due to debounce
|
||||||
runs(function () {
|
runs(function () {
|
||||||
expect($roster.find('dd:visible').length).toBe(15);
|
expect($roster.find('dd:visible').length).toBe(15);
|
||||||
expect($roster.find('dt:visible').length).toBe(5);
|
expect($roster.find('dt:visible').length).toBe(5);
|
||||||
|
@ -262,12 +279,14 @@
|
||||||
utils.createGroupedContacts();
|
utils.createGroupedContacts();
|
||||||
var $filter = converse.rosterview.$('.roster-filter');
|
var $filter = converse.rosterview.$('.roster-filter');
|
||||||
runs (function () {
|
runs (function () {
|
||||||
|
converse.rosterview.filter_view.delegateEvents();
|
||||||
$filter.val("xxx");
|
$filter.val("xxx");
|
||||||
$filter.trigger('keydown');
|
$filter.trigger('keydown');
|
||||||
expect($filter.hasClass("x")).toBeFalsy();
|
expect($filter.hasClass("x")).toBeFalsy();
|
||||||
});
|
});
|
||||||
waits(350); // Needed, due to debounce
|
waits(550); // Needed, due to debounce
|
||||||
runs (function () {
|
runs (function () {
|
||||||
|
$filter = converse.rosterview.$('.roster-filter');
|
||||||
expect($filter.hasClass("x")).toBeTruthy();
|
expect($filter.hasClass("x")).toBeTruthy();
|
||||||
$filter.addClass("onX").click();
|
$filter.addClass("onX").click();
|
||||||
expect($filter.val()).toBe("");
|
expect($filter.val()).toBe("");
|
||||||
|
|
|
@ -324,7 +324,7 @@
|
||||||
onControlBoxToggleHidden: function () {
|
onControlBoxToggleHidden: function () {
|
||||||
this.$el.show('fast', function () {
|
this.$el.show('fast', function () {
|
||||||
if (converse.rosterview) {
|
if (converse.rosterview) {
|
||||||
converse.rosterview.update();
|
converse.rosterview.updateOnlineCount();
|
||||||
}
|
}
|
||||||
utils.refreshWebkit();
|
utils.refreshWebkit();
|
||||||
converse.emit('controlBoxOpened', this);
|
converse.emit('controlBoxOpened', this);
|
||||||
|
|
|
@ -779,20 +779,6 @@
|
||||||
.c('item', {jid: this.get('jid'), subscription: "remove"});
|
.c('item', {jid: this.get('jid'), subscription: "remove"});
|
||||||
converse.connection.sendIQ(iq, callback, callback);
|
converse.connection.sendIQ(iq, callback, callback);
|
||||||
return this;
|
return this;
|
||||||
},
|
|
||||||
|
|
||||||
showInRoster: function () {
|
|
||||||
var chatStatus = this.get('chat_status');
|
|
||||||
if ((converse.show_only_online_users && chatStatus !== 'online') || (converse.hide_offline_users && chatStatus === 'offline')) {
|
|
||||||
// If pending or requesting, show
|
|
||||||
if ((this.get('ask') === 'subscribe') ||
|
|
||||||
(this.get('subscription') === 'from') ||
|
|
||||||
(this.get('requesting') === true)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -67,16 +67,126 @@
|
||||||
HEADER_WEIGHTS[HEADER_REQUESTING_CONTACTS] = 2;
|
HEADER_WEIGHTS[HEADER_REQUESTING_CONTACTS] = 2;
|
||||||
HEADER_WEIGHTS[HEADER_PENDING_CONTACTS] = 3;
|
HEADER_WEIGHTS[HEADER_PENDING_CONTACTS] = 3;
|
||||||
|
|
||||||
|
converse.RosterFilter = Backbone.Model.extend({
|
||||||
|
initialize: function () {
|
||||||
|
this.set({
|
||||||
|
'filter_text': '',
|
||||||
|
'filter_type': 'contacts',
|
||||||
|
'chat_state': ''
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
converse.RosterFilterView = Backbone.View.extend({
|
||||||
|
tagName: 'span',
|
||||||
|
events: {
|
||||||
|
"keydown .roster-filter": "liveFilter",
|
||||||
|
"click .onX": "clearFilter",
|
||||||
|
"mousemove .x": "toggleX",
|
||||||
|
"change .filter-type": "changeTypeFilter",
|
||||||
|
"change .state-type": "changeChatStateFilter"
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function () {
|
||||||
|
this.model.on('change', this.render, this);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function () {
|
||||||
|
this.$el.html(converse.templates.roster(
|
||||||
|
_.extend(this.model.toJSON(), {
|
||||||
|
placeholder: __('Type to filter'),
|
||||||
|
label_contacts: LABEL_CONTACTS,
|
||||||
|
label_groups: LABEL_GROUPS,
|
||||||
|
label_state: __('State'),
|
||||||
|
label_any: __('Any'),
|
||||||
|
label_online: __('Online'),
|
||||||
|
label_chatty: __('Chatty'),
|
||||||
|
label_busy: __('Busy'),
|
||||||
|
label_away: __('Away'),
|
||||||
|
label_xa: __('Extended Away'),
|
||||||
|
label_offline: __('Offline')
|
||||||
|
})
|
||||||
|
));
|
||||||
|
var $roster_filter = this.$('.roster-filter');
|
||||||
|
$roster_filter[this.tog($roster_filter.val())]('x');
|
||||||
|
return this.$el;
|
||||||
|
},
|
||||||
|
|
||||||
|
tog: function (v) {
|
||||||
|
return v?'addClass':'removeClass';
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleX: function (ev) {
|
||||||
|
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
||||||
|
var el = ev.target;
|
||||||
|
$(el)[this.tog(el.offsetWidth-18 < ev.clientX-el.getBoundingClientRect().left)]('onX');
|
||||||
|
},
|
||||||
|
|
||||||
|
changeChatStateFilter: function (ev) {
|
||||||
|
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
||||||
|
this.model.save({
|
||||||
|
'chat_state': this.$('.state-type').val()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
changeTypeFilter: function (ev) {
|
||||||
|
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
||||||
|
var type = ev.target.value;
|
||||||
|
if (type === 'state') {
|
||||||
|
this.model.save({
|
||||||
|
'filter_type': type,
|
||||||
|
'chat_state': this.$('.state-type').val()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.model.save({
|
||||||
|
'filter_type': type,
|
||||||
|
'filter_text': this.$('.roster-filter').val(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
liveFilter: _.debounce(function (ev) {
|
||||||
|
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
||||||
|
this.model.save({
|
||||||
|
'filter_type': this.$('.filter-type').val(),
|
||||||
|
'filter_text': this.$('.roster-filter').val()
|
||||||
|
});
|
||||||
|
}, 250),
|
||||||
|
|
||||||
|
show: function () {
|
||||||
|
if (this.$el.is(':visible')) { return this; }
|
||||||
|
this.$el.show();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
hide: function () {
|
||||||
|
if (!this.$el.is(':visible')) { return this; }
|
||||||
|
if (this.$('.roster-filter').val().length > 0) {
|
||||||
|
// Don't hide if user is currently filtering.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.model.save({
|
||||||
|
'filter_text': '',
|
||||||
|
'chat_state': ''
|
||||||
|
});
|
||||||
|
this.$el.hide();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
clearFilter: function (ev) {
|
||||||
|
if (ev && ev.preventDefault) {
|
||||||
|
ev.preventDefault();
|
||||||
|
$(ev.target).removeClass('x onX').val('');
|
||||||
|
}
|
||||||
|
this.model.save({
|
||||||
|
'filter_text': ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
converse.RosterView = Backbone.Overview.extend({
|
converse.RosterView = Backbone.Overview.extend({
|
||||||
tagName: 'div',
|
tagName: 'div',
|
||||||
id: 'converse-roster',
|
id: 'converse-roster',
|
||||||
events: {
|
|
||||||
"keydown .roster-filter": "liveFilter",
|
|
||||||
"click .onX": "clearFilter",
|
|
||||||
"mousemove .x": "togglePointer",
|
|
||||||
"change .filter-type": "changeFilterType"
|
|
||||||
},
|
|
||||||
|
|
||||||
initialize: function () {
|
initialize: function () {
|
||||||
this.roster_handler_ref = this.registerRosterHandler();
|
this.roster_handler_ref = this.registerRosterHandler();
|
||||||
|
@ -89,8 +199,42 @@
|
||||||
this.model.on("add", this.onGroupAdd, this);
|
this.model.on("add", this.onGroupAdd, this);
|
||||||
this.model.on("reset", this.reset, this);
|
this.model.on("reset", this.reset, this);
|
||||||
this.$roster = $('<dl class="roster-contacts" style="display: none;"></dl>');
|
this.$roster = $('<dl class="roster-contacts" style="display: none;"></dl>');
|
||||||
|
// Create a model on which we can store filter properties
|
||||||
|
var model = new converse.RosterFilter();
|
||||||
|
model.id = b64_sha1('converse.rosterfilter'+converse.bare_jid);
|
||||||
|
model.browserStorage = new Backbone.BrowserStorage.local(this.filter.id);
|
||||||
|
this.filter_view = new converse.RosterFilterView({'model': model});
|
||||||
|
this.filter_view.model.on('change', this.updateFilter, this);
|
||||||
|
this.filter_view.model.fetch();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
render: function () {
|
||||||
|
this.$el.html(this.filter_view.render());
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateFilter: _.debounce(function () {
|
||||||
|
/* Filter the roster again.
|
||||||
|
* Called whenever the filter settings have been changed or
|
||||||
|
* when contacts have been added, removed or changed.
|
||||||
|
*
|
||||||
|
* Debounced so that it doesn't get called for every
|
||||||
|
* contact fetched from browser storage.
|
||||||
|
*/
|
||||||
|
converse.log('updateFilter called!!!!!!');
|
||||||
|
var type = this.filter_view.model.get('filter_type');
|
||||||
|
if (type === 'state') {
|
||||||
|
this.filter(this.filter_view.model.get('chat_state'), type);
|
||||||
|
} else {
|
||||||
|
this.filter(this.filter_view.model.get('filter_text'), type);
|
||||||
|
}
|
||||||
|
}, 100),
|
||||||
|
|
||||||
unregisterHandlers: function () {
|
unregisterHandlers: function () {
|
||||||
converse.connection.deleteHandler(this.roster_handler_ref);
|
converse.connection.deleteHandler(this.roster_handler_ref);
|
||||||
delete this.roster_handler_ref;
|
delete this.roster_handler_ref;
|
||||||
|
@ -100,28 +244,30 @@
|
||||||
delete this.presence_ref;
|
delete this.presence_ref;
|
||||||
},
|
},
|
||||||
|
|
||||||
update: _.debounce(function () {
|
updateOnlineCount: function () {
|
||||||
var $count = $('#online-count');
|
var $count = $('#online-count');
|
||||||
$count.text('('+converse.roster.getNumOnlineContacts()+')');
|
$count.text('('+converse.roster.getNumOnlineContacts()+')');
|
||||||
if (!$count.is(':visible')) {
|
if (!$count.is(':visible')) {
|
||||||
$count.show();
|
$count.show();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
update: _.debounce(function () {
|
||||||
|
this.updateOnlineCount();
|
||||||
if (this.$roster.parent().length === 0) {
|
if (this.$roster.parent().length === 0) {
|
||||||
this.$el.append(this.$roster.show());
|
this.$el.append(this.$roster.show());
|
||||||
}
|
}
|
||||||
return this.showHideFilter();
|
return this.showHideFilter();
|
||||||
}, converse.animate ? 100 : 0),
|
}, converse.animate ? 100 : 0),
|
||||||
|
|
||||||
render: function () {
|
showHideFilter: function () {
|
||||||
this.$el.html(converse.templates.roster({
|
if (!this.$el.is(':visible')) {
|
||||||
placeholder: __('Type to filter'),
|
return;
|
||||||
label_contacts: LABEL_CONTACTS,
|
}
|
||||||
label_groups: LABEL_GROUPS
|
if (this.$roster.hasScrollBar()) {
|
||||||
}));
|
this.filter_view.show();
|
||||||
if (!converse.allow_contact_requests) {
|
} else {
|
||||||
// XXX: if we ever support live editing of config then
|
this.filter_view.hide();
|
||||||
// we'll need to be able to remove this class on the fly.
|
|
||||||
this.$el.addClass('no-contact-requests');
|
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
@ -163,26 +309,15 @@
|
||||||
return 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) {
|
filter: function (query, type) {
|
||||||
|
// First we make sure the filter is restored to its
|
||||||
|
// original state
|
||||||
|
_.each(this.getAll(), function (view) {
|
||||||
|
if (view.model.contacts.length > 0) {
|
||||||
|
view.show().filter('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Now we can filter
|
||||||
query = query.toLowerCase();
|
query = query.toLowerCase();
|
||||||
if (type === 'groups') {
|
if (type === 'groups') {
|
||||||
_.each(this.getAll(), function (view, idx) {
|
_.each(this.getAll(), function (view, idx) {
|
||||||
|
@ -199,46 +334,6 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
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 () {
|
reset: function () {
|
||||||
converse.roster.reset();
|
converse.roster.reset();
|
||||||
this.removeAll();
|
this.removeAll();
|
||||||
|
@ -288,6 +383,7 @@
|
||||||
|
|
||||||
onContactAdd: function (contact) {
|
onContactAdd: function (contact) {
|
||||||
this.addRosterContact(contact).update();
|
this.addRosterContact(contact).update();
|
||||||
|
this.updateFilter();
|
||||||
},
|
},
|
||||||
|
|
||||||
onContactChange: function (contact) {
|
onContactChange: function (contact) {
|
||||||
|
@ -305,7 +401,7 @@
|
||||||
if (_.has(contact.changed, 'subscription') && contact.changed.requesting === 'true') {
|
if (_.has(contact.changed, 'subscription') && contact.changed.requesting === 'true') {
|
||||||
this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS);
|
this.addContactToGroup(contact, HEADER_REQUESTING_CONTACTS);
|
||||||
}
|
}
|
||||||
this.liveFilter();
|
this.updateFilter();
|
||||||
},
|
},
|
||||||
|
|
||||||
updateChatBox: function (contact) {
|
updateChatBox: function (contact) {
|
||||||
|
@ -438,11 +534,9 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function () {
|
render: function () {
|
||||||
if (!this.model.showInRoster()) {
|
if (!this.mayBeShown()) {
|
||||||
this.$el.hide();
|
this.$el.hide();
|
||||||
return this;
|
return this;
|
||||||
} else if (this.$el[0].style.display === "none") {
|
|
||||||
this.$el.show();
|
|
||||||
}
|
}
|
||||||
var item = this.model,
|
var item = this.model,
|
||||||
ask = item.get('ask'),
|
ask = item.get('ask'),
|
||||||
|
@ -509,6 +603,45 @@
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isGroupCollapsed: function () {
|
||||||
|
/* Check whether the group in which this contact appears is
|
||||||
|
* collapsed.
|
||||||
|
*/
|
||||||
|
// XXX: this sucks and is fragile.
|
||||||
|
// It's because I tried to do the "right thing"
|
||||||
|
// and use definition lists to represent roster groups.
|
||||||
|
// If roster group items were inside the group elements, we
|
||||||
|
// would simplify things by not having to check whether the
|
||||||
|
// group is collapsed or not.
|
||||||
|
var name = this.$el.prevAll('dt:first').data('group');
|
||||||
|
var group = converse.rosterview.model.where({'name': name})[0];
|
||||||
|
if (group.get('state') === converse.CLOSED) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
mayBeShown: function () {
|
||||||
|
/* Return a boolean indicating whether this contact should
|
||||||
|
* generally be visible in the roster.
|
||||||
|
*
|
||||||
|
* It doesn't check for the more specific case of whether
|
||||||
|
* the group it's in is collapsed (see isGroupCollapsed).
|
||||||
|
*/
|
||||||
|
var chatStatus = this.model.get('chat_status');
|
||||||
|
if ((converse.show_only_online_users && chatStatus !== 'online') ||
|
||||||
|
(converse.hide_offline_users && chatStatus === 'offline')) {
|
||||||
|
// If pending or requesting, show
|
||||||
|
if ((this.model.get('ask') === 'subscribe') ||
|
||||||
|
(this.model.get('subscription') === 'from') ||
|
||||||
|
(this.model.get('requesting') === true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
openChat: function (ev) {
|
openChat: function (ev) {
|
||||||
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
||||||
return converse.chatboxviews.showChat(this.model.attributes);
|
return converse.chatboxviews.showChat(this.model.attributes);
|
||||||
|
@ -606,7 +739,7 @@
|
||||||
var view = new converse.RosterContactView({model: contact});
|
var view = new converse.RosterContactView({model: contact});
|
||||||
this.add(contact.get('id'), view);
|
this.add(contact.get('id'), view);
|
||||||
view = this.positionContact(contact).render();
|
view = this.positionContact(contact).render();
|
||||||
if (contact.showInRoster()) {
|
if (view.mayBeShown()) {
|
||||||
if (this.model.get('state') === converse.CLOSED) {
|
if (this.model.get('state') === converse.CLOSED) {
|
||||||
if (view.$el[0].style.display !== "none") { view.$el.hide(); }
|
if (view.$el[0].style.display !== "none") { view.$el.hide(); }
|
||||||
if (!this.$el.is(':visible')) { this.$el.show(); }
|
if (!this.$el.is(':visible')) { this.$el.show(); }
|
||||||
|
@ -635,18 +768,19 @@
|
||||||
|
|
||||||
show: function () {
|
show: function () {
|
||||||
this.$el.show();
|
this.$el.show();
|
||||||
_.each(this.getAll(), function (contactView) {
|
_.each(this.getAll(), function (view) {
|
||||||
if (contactView.model.showInRoster()) {
|
if (view.mayBeShown() && !view.isGroupCollapsed()) {
|
||||||
contactView.$el.show();
|
view.$el.show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
hide: function () {
|
hide: function () {
|
||||||
this.$el.nextUntil('dt').addBack().hide();
|
this.$el.nextUntil('dt').addBack().hide();
|
||||||
},
|
},
|
||||||
|
|
||||||
filter: function (q) {
|
filter: function (q, type) {
|
||||||
/* Filter the group's contacts based on the query "q".
|
/* Filter the group's contacts based on the query "q".
|
||||||
* The query is matched against the contact's full name.
|
* The query is matched against the contact's full name.
|
||||||
* If all contacts are filtered out (i.e. hidden), then the
|
* If all contacts are filtered out (i.e. hidden), then the
|
||||||
|
@ -656,16 +790,26 @@
|
||||||
if (q.length === 0) {
|
if (q.length === 0) {
|
||||||
if (this.model.get('state') === converse.OPENED) {
|
if (this.model.get('state') === converse.OPENED) {
|
||||||
this.model.contacts.each(function (item) {
|
this.model.contacts.each(function (item) {
|
||||||
if (item.showInRoster()) {
|
var view = this.get(item.get('id'));
|
||||||
this.get(item.get('id')).$el.show();
|
if (view.mayBeShown() && !view.isGroupCollapsed()) {
|
||||||
|
view.$el.show();
|
||||||
}
|
}
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
}
|
}
|
||||||
this.showIfNecessary();
|
this.showIfNecessary();
|
||||||
} else {
|
} else {
|
||||||
q = q.toLowerCase();
|
q = q.toLowerCase();
|
||||||
matches = this.model.contacts.filter(utils.contains.not('fullname', q));
|
if (type === 'state') {
|
||||||
if (matches.length === this.model.contacts.length) { // hide the whole group
|
matches = this.model.contacts.filter(
|
||||||
|
utils.contains.not('chat_status', q)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
matches = this.model.contacts.filter(
|
||||||
|
utils.contains.not('fullname', q)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (matches.length === this.model.contacts.length) {
|
||||||
|
// hide the whole group
|
||||||
this.hide();
|
this.hide();
|
||||||
} else {
|
} else {
|
||||||
_.each(matches, function (item) {
|
_.each(matches, function (item) {
|
||||||
|
@ -696,7 +840,7 @@
|
||||||
$el.removeClass("icon-closed").addClass("icon-opened");
|
$el.removeClass("icon-closed").addClass("icon-opened");
|
||||||
this.model.save({state: converse.OPENED});
|
this.model.save({state: converse.OPENED});
|
||||||
this.filter(
|
this.filter(
|
||||||
converse.rosterview.$('.roster-filter').val(),
|
converse.rosterview.$('.roster-filter').val() || '',
|
||||||
converse.rosterview.$('.filter-type').val()
|
converse.rosterview.$('.filter-type').val()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,28 @@
|
||||||
<form class="pure-form roster-filter-group input-button-group">
|
<form class="pure-form roster-filter-group input-button-group">
|
||||||
<input style="display: none;" class="roster-filter" placeholder="{{placeholder}}">
|
<input value="{{filter_text}}" class="roster-filter"
|
||||||
<select style="display: none;" class="filter-type">
|
placeholder="{{placeholder}}"
|
||||||
<option value="contacts">{{label_contacts}}</option>
|
{[ if (filter_type === 'state') { ]} style="display: none" {[ } ]} >
|
||||||
<option value="groups">{{label_groups}}</option>
|
<select class="state-type" {[ if (filter_type !== 'state') { ]} style="display: none" {[ } ]} >
|
||||||
|
<option value="">{{label_any}}</option>
|
||||||
|
<option {[ if (chat_state === 'online') { ]} selected="selected" {[ } ]}
|
||||||
|
value="online">{{label_online}}</option>
|
||||||
|
<option {[ if (chat_state === 'chatty') { ]} selected="selected" {[ } ]}
|
||||||
|
value="chatty">{{label_chatty}}</option>
|
||||||
|
<option {[ if (chat_state === 'dnd') { ]} selected="selected" {[ } ]}
|
||||||
|
value="dnd">{{label_busy}}</option>
|
||||||
|
<option {[ if (chat_state === 'away') { ]} selected="selected" {[ } ]}
|
||||||
|
value="away">{{label_away}}</option>
|
||||||
|
<option {[ if (chat_state === 'xa') { ]} selected="selected" {[ } ]}
|
||||||
|
value="xa">{{label_xa}}</option>
|
||||||
|
<option {[ if (chat_state === 'offline') { ]} selected="selected" {[ } ]}
|
||||||
|
value="offline">{{label_offline}}</option>
|
||||||
|
</select>
|
||||||
|
<select class="filter-type">
|
||||||
|
<option {[ if (filter_type === 'contacts') { ]} selected="selected" {[ } ]}
|
||||||
|
value="contacts">{{label_contacts}}</option>
|
||||||
|
<option {[ if (filter_type === 'groups') { ]} selected="selected" {[ } ]}
|
||||||
|
value="groups">{{label_groups}}</option>
|
||||||
|
<option {[ if (filter_type === 'state') { ]} selected="selected" {[ } ]}
|
||||||
|
value="state">{{label_state}}</option>
|
||||||
</select>
|
</select>
|
||||||
</form>
|
</form>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user