2019-07-11 10:48:52 +02:00
|
|
|
/**
|
|
|
|
* @module converse-rosterview
|
2020-01-26 16:21:20 +01:00
|
|
|
* @copyright 2020, the Converse.js contributors
|
2020-01-23 10:18:41 +01:00
|
|
|
* @license Mozilla Public License (MPLv2)
|
2019-07-11 10:48:52 +02:00
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
import "@converse/headless/converse-chatboxes";
|
2019-05-23 14:26:20 +02:00
|
|
|
import "@converse/headless/converse-roster";
|
2018-10-23 03:41:38 +02:00
|
|
|
import "converse-modal";
|
2019-10-09 16:01:38 +02:00
|
|
|
import "formdata-polyfill";
|
2019-12-17 14:38:12 +01:00
|
|
|
import { compact, debounce, has, isString, uniq, without } from "lodash";
|
2020-01-30 12:57:11 +01:00
|
|
|
import { View } from 'skeletor.js/src/view.js';
|
2019-09-19 16:54:55 +02:00
|
|
|
import { Model } from 'skeletor.js/src/model.js';
|
|
|
|
import { OrderedListView } from "skeletor.js/src/overview";
|
2018-10-23 03:41:38 +02:00
|
|
|
import converse from "@converse/headless/converse-core";
|
2019-11-06 11:01:34 +01:00
|
|
|
import log from "@converse/headless/log";
|
2020-01-23 10:18:41 +01:00
|
|
|
import tpl_add_contact_modal from "templates/add_contact_modal.js";
|
2018-10-23 03:41:38 +02:00
|
|
|
import tpl_group_header from "templates/group_header.html";
|
|
|
|
import tpl_pending_contact from "templates/pending_contact.html";
|
|
|
|
import tpl_requesting_contact from "templates/requesting_contact.html";
|
|
|
|
import tpl_roster from "templates/roster.html";
|
2020-01-30 12:03:28 +01:00
|
|
|
import tpl_roster_filter from "templates/roster_filter.js";
|
2018-10-23 03:41:38 +02:00
|
|
|
import tpl_roster_item from "templates/roster_item.html";
|
|
|
|
|
2020-01-30 12:03:28 +01:00
|
|
|
const { Strophe } = converse.env;
|
2018-10-23 03:41:38 +02:00
|
|
|
const u = converse.env.utils;
|
|
|
|
|
|
|
|
|
|
|
|
converse.plugins.add('converse-rosterview', {
|
|
|
|
|
2018-11-27 15:28:31 +01:00
|
|
|
dependencies: ["converse-roster", "converse-modal", "converse-chatboxviews"],
|
2018-10-23 03:41:38 +02:00
|
|
|
|
|
|
|
initialize () {
|
|
|
|
/* The initialize function gets called as soon as the plugin is
|
|
|
|
* loaded by converse.js's plugin machinery.
|
|
|
|
*/
|
|
|
|
const { _converse } = this,
|
|
|
|
{ __ } = _converse;
|
|
|
|
|
|
|
|
_converse.api.settings.update({
|
2019-03-28 14:34:12 +01:00
|
|
|
'autocomplete_add_contact': true,
|
2018-10-23 03:41:38 +02:00
|
|
|
'allow_chat_pending_contacts': true,
|
|
|
|
'allow_contact_removal': true,
|
|
|
|
'hide_offline_users': false,
|
|
|
|
'roster_groups': true,
|
|
|
|
'show_toolbar': true,
|
2019-03-28 14:34:12 +01:00
|
|
|
'xhr_user_search_url': null,
|
2018-10-23 03:41:38 +02:00
|
|
|
});
|
|
|
|
_converse.api.promises.add('rosterViewInitialized');
|
|
|
|
|
|
|
|
const 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')
|
|
|
|
};
|
2018-02-23 15:45:44 +01:00
|
|
|
|
2018-03-26 17:55:12 +02:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
_converse.AddContactModal = _converse.BootstrapModal.extend({
|
2020-01-23 10:18:41 +01:00
|
|
|
id: "add-contact-modal",
|
2018-10-23 03:41:38 +02:00
|
|
|
events: {
|
|
|
|
'submit form': 'addContactFromForm'
|
|
|
|
},
|
|
|
|
|
|
|
|
initialize () {
|
|
|
|
_converse.BootstrapModal.prototype.initialize.apply(this, arguments);
|
2019-09-06 14:34:59 +02:00
|
|
|
this.listenTo(this.model, 'change', this.render);
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
toHTML () {
|
|
|
|
const label_nickname = _converse.xhr_user_search_url ? __('Contact name') : __('Optional nickname');
|
2019-04-29 09:07:15 +02:00
|
|
|
return tpl_add_contact_modal(Object.assign(this.model.toJSON(), {
|
2018-10-23 03:41:38 +02:00
|
|
|
'_converse': _converse,
|
|
|
|
'label_nickname': label_nickname,
|
|
|
|
}));
|
|
|
|
},
|
|
|
|
|
|
|
|
afterRender () {
|
2019-12-17 14:38:12 +01:00
|
|
|
if (_converse.xhr_user_search_url && isString(_converse.xhr_user_search_url)) {
|
2019-03-28 10:47:14 +01:00
|
|
|
this.initXHRAutoComplete();
|
2018-10-23 03:41:38 +02:00
|
|
|
} else {
|
2019-03-28 10:47:14 +01:00
|
|
|
this.initJIDAutoComplete();
|
2018-02-23 15:45:44 +01:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
const jid_input = this.el.querySelector('input[name="jid"]');
|
2019-03-01 11:42:09 +01:00
|
|
|
this.el.addEventListener('shown.bs.modal', () => jid_input.focus(), false);
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2018-02-23 15:45:44 +01:00
|
|
|
|
2019-03-28 10:47:14 +01:00
|
|
|
initJIDAutoComplete () {
|
2019-03-28 14:34:12 +01:00
|
|
|
if (!_converse.autocomplete_add_contact) {
|
|
|
|
return;
|
|
|
|
}
|
2019-03-28 10:47:14 +01:00
|
|
|
const el = this.el.querySelector('.suggestion-box__jid').parentElement;
|
|
|
|
this.jid_auto_complete = new _converse.AutoComplete(el, {
|
2019-03-01 11:42:09 +01:00
|
|
|
'data': (text, input) => `${input.slice(0, input.indexOf("@"))}@${text}`,
|
2019-03-28 10:47:14 +01:00
|
|
|
'filter': _converse.FILTER_STARTSWITH,
|
2019-12-17 14:38:12 +01:00
|
|
|
'list': uniq(_converse.roster.map(item => Strophe.getDomainFromJid(item.get('jid'))))
|
2018-10-23 03:41:38 +02:00
|
|
|
});
|
|
|
|
},
|
2018-02-23 15:45:44 +01:00
|
|
|
|
2019-03-28 10:47:14 +01:00
|
|
|
initXHRAutoComplete () {
|
2019-03-28 14:34:12 +01:00
|
|
|
if (!_converse.autocomplete_add_contact) {
|
|
|
|
return this.initXHRFetch();
|
|
|
|
}
|
2019-03-28 10:47:14 +01:00
|
|
|
const el = this.el.querySelector('.suggestion-box__name').parentElement;
|
|
|
|
this.name_auto_complete = new _converse.AutoComplete(el, {
|
|
|
|
'auto_evaluate': false,
|
|
|
|
'filter': _converse.FILTER_STARTSWITH,
|
2018-10-23 03:41:38 +02:00
|
|
|
'list': []
|
|
|
|
});
|
|
|
|
const xhr = new window.XMLHttpRequest();
|
|
|
|
// `open` must be called after `onload` for mock/testing purposes.
|
2019-03-28 10:47:14 +01:00
|
|
|
xhr.onload = () => {
|
2018-10-23 03:41:38 +02:00
|
|
|
if (xhr.responseText) {
|
2019-03-28 10:47:14 +01:00
|
|
|
const r = xhr.responseText;
|
|
|
|
this.name_auto_complete.list = JSON.parse(r).map(i => ({'label': i.fullname || i.jid, 'value': i.jid}));
|
|
|
|
this.name_auto_complete.auto_completing = true;
|
|
|
|
this.name_auto_complete.evaluate();
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
};
|
2019-03-28 10:47:14 +01:00
|
|
|
const input_el = this.el.querySelector('input[name="name"]');
|
2019-12-17 14:38:12 +01:00
|
|
|
input_el.addEventListener('input', debounce(() => {
|
2019-07-01 11:47:18 +02:00
|
|
|
xhr.open("GET", `${_converse.xhr_user_search_url}q=${encodeURIComponent(input_el.value)}`, true);
|
2018-10-23 03:41:38 +02:00
|
|
|
xhr.send()
|
|
|
|
} , 300));
|
2019-03-28 14:34:12 +01:00
|
|
|
this.name_auto_complete.on('suggestion-box-selectcomplete', ev => {
|
|
|
|
this.el.querySelector('input[name="name"]').value = ev.text.label;
|
|
|
|
this.el.querySelector('input[name="jid"]').value = ev.text.value;
|
|
|
|
});
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2016-03-13 17:40:52 +01:00
|
|
|
|
2019-03-28 14:34:12 +01:00
|
|
|
initXHRFetch () {
|
|
|
|
this.xhr = new window.XMLHttpRequest();
|
|
|
|
this.xhr.onload = () => {
|
|
|
|
if (this.xhr.responseText) {
|
|
|
|
const r = this.xhr.responseText;
|
|
|
|
const list = JSON.parse(r).map(i => ({'label': i.fullname || i.jid, 'value': i.jid}));
|
|
|
|
if (list.length !== 1) {
|
2019-03-28 15:32:50 +01:00
|
|
|
const el = this.el.querySelector('.invalid-feedback');
|
2019-03-28 14:34:12 +01:00
|
|
|
el.textContent = __('Sorry, could not find a contact with that name')
|
|
|
|
u.addClass('d-block', el);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const jid = list[0].value;
|
|
|
|
if (this.validateSubmission(jid)) {
|
|
|
|
const form = this.el.querySelector('form');
|
|
|
|
const name = list[0].label;
|
|
|
|
this.afterSubmission(form, jid, name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
validateSubmission (jid) {
|
2019-03-28 15:32:50 +01:00
|
|
|
const el = this.el.querySelector('.invalid-feedback');
|
2019-12-17 14:38:12 +01:00
|
|
|
if (!jid || compact(jid.split('@')).length < 2) {
|
2018-10-23 03:41:38 +02:00
|
|
|
u.addClass('is-invalid', this.el.querySelector('input[name="jid"]'));
|
2019-03-28 15:32:50 +01:00
|
|
|
u.addClass('d-block', el);
|
|
|
|
return false;
|
2019-03-28 15:54:47 +01:00
|
|
|
} else if (_converse.roster.get(Strophe.getBareJidFromJid(jid))) {
|
|
|
|
el.textContent = __('This contact has already been added')
|
|
|
|
u.addClass('d-block', el);
|
|
|
|
return false;
|
2019-03-28 14:34:12 +01:00
|
|
|
}
|
2019-03-28 15:32:50 +01:00
|
|
|
u.removeClass('d-block', el);
|
2019-03-28 14:34:12 +01:00
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
|
|
|
afterSubmission (form, jid, name) {
|
|
|
|
_converse.roster.addAndSubscribe(jid, name);
|
|
|
|
this.model.clear();
|
|
|
|
this.modal.hide();
|
|
|
|
},
|
|
|
|
|
|
|
|
addContactFromForm (ev) {
|
|
|
|
ev.preventDefault();
|
|
|
|
const data = new FormData(ev.target),
|
2019-06-19 09:21:46 +02:00
|
|
|
jid = (data.get('jid') || '').trim();
|
2019-03-28 14:34:12 +01:00
|
|
|
|
2019-12-17 14:38:12 +01:00
|
|
|
if (!jid && _converse.xhr_user_search_url && isString(_converse.xhr_user_search_url)) {
|
2019-03-28 14:34:12 +01:00
|
|
|
const input_el = this.el.querySelector('input[name="name"]');
|
2019-07-01 11:47:18 +02:00
|
|
|
this.xhr.open("GET", `${_converse.xhr_user_search_url}q=${encodeURIComponent(input_el.value)}`, true);
|
2019-03-28 14:34:12 +01:00
|
|
|
this.xhr.send()
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (this.validateSubmission(jid)) {
|
|
|
|
this.afterSubmission(ev.target, jid, data.get('name'));
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2019-09-19 16:54:55 +02:00
|
|
|
_converse.RosterFilter = Model.extend({
|
2018-10-23 03:41:38 +02:00
|
|
|
initialize () {
|
|
|
|
this.set({
|
|
|
|
'filter_text': '',
|
|
|
|
'filter_type': 'contacts',
|
2019-12-17 14:27:56 +01:00
|
|
|
'chat_state': 'online'
|
2018-10-23 03:41:38 +02:00
|
|
|
});
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2020-01-30 12:57:11 +01:00
|
|
|
_converse.RosterFilterView = View.extend({
|
2020-01-30 12:03:28 +01:00
|
|
|
tagName: 'span',
|
2018-10-23 03:41:38 +02:00
|
|
|
|
|
|
|
initialize () {
|
2019-09-06 14:34:59 +02:00
|
|
|
this.listenTo(this.model, 'change:filter_type', this.render);
|
|
|
|
this.listenTo(this.model, 'change:filter_text', this.render);
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
toHTML () {
|
|
|
|
return tpl_roster_filter(
|
2019-04-29 09:07:15 +02:00
|
|
|
Object.assign(this.model.toJSON(), {
|
2018-10-23 03:41:38 +02:00
|
|
|
visible: this.shouldBeVisible(),
|
|
|
|
placeholder: __('Filter'),
|
|
|
|
title_contact_filter: __('Filter by contact name'),
|
|
|
|
title_group_filter: __('Filter by group name'),
|
|
|
|
title_status_filter: __('Filter by status'),
|
|
|
|
label_any: __('Any'),
|
|
|
|
label_unread_messages: __('Unread'),
|
|
|
|
label_online: __('Online'),
|
|
|
|
label_chatty: __('Chatty'),
|
|
|
|
label_busy: __('Busy'),
|
|
|
|
label_away: __('Away'),
|
|
|
|
label_xa: __('Extended Away'),
|
2020-01-30 12:03:28 +01:00
|
|
|
label_offline: __('Offline'),
|
|
|
|
changeChatStateFilter: ev => this.changeChatStateFilter(ev),
|
|
|
|
changeTypeFilter: ev => this.changeTypeFilter(ev),
|
|
|
|
clearFilter: ev => this.clearFilter(ev),
|
|
|
|
liveFilter: ev => this.liveFilter(ev),
|
|
|
|
submitFilter: ev => this.submitFilter(ev),
|
2018-10-23 03:41:38 +02:00
|
|
|
}));
|
|
|
|
},
|
|
|
|
|
|
|
|
changeChatStateFilter (ev) {
|
2020-01-30 12:03:28 +01:00
|
|
|
ev && ev.preventDefault();
|
2019-12-17 14:27:56 +01:00
|
|
|
this.model.save({'chat_state': this.el.querySelector('.state-type').value});
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
changeTypeFilter (ev) {
|
2020-01-30 12:03:28 +01:00
|
|
|
ev && ev.preventDefault();
|
2018-10-23 03:41:38 +02:00
|
|
|
const type = ev.target.dataset.type;
|
|
|
|
if (type === 'state') {
|
2016-04-02 13:30:54 +02:00
|
|
|
this.model.save({
|
2018-10-23 03:41:38 +02:00
|
|
|
'filter_type': type,
|
2017-03-30 16:45:52 +02:00
|
|
|
'chat_state': this.el.querySelector('.state-type').value
|
2016-04-02 13:30:54 +02:00
|
|
|
});
|
2018-10-23 03:41:38 +02:00
|
|
|
} else {
|
2016-04-02 13:30:54 +02:00
|
|
|
this.model.save({
|
2018-10-23 03:41:38 +02:00
|
|
|
'filter_type': type,
|
2017-03-30 16:45:52 +02:00
|
|
|
'filter_text': this.el.querySelector('.roster-filter').value
|
2016-04-02 13:30:54 +02:00
|
|
|
});
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
},
|
2016-04-02 13:30:54 +02:00
|
|
|
|
2019-12-17 14:38:12 +01:00
|
|
|
liveFilter: debounce(function () {
|
2020-01-30 12:03:28 +01:00
|
|
|
this.model.save({'filter_text': this.el.querySelector('.roster-filter').value});
|
2018-10-23 03:41:38 +02:00
|
|
|
}, 250),
|
2017-02-13 16:05:43 +01:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
submitFilter (ev) {
|
2020-01-30 12:03:28 +01:00
|
|
|
ev && ev.preventDefault();
|
2018-10-23 03:41:38 +02:00
|
|
|
this.liveFilter();
|
|
|
|
},
|
2016-05-24 09:58:54 +02:00
|
|
|
|
2020-01-30 12:03:28 +01:00
|
|
|
/**
|
|
|
|
* Returns true if the filter is enabled (i.e. if the user
|
|
|
|
* has added values to the filter).
|
|
|
|
* @private
|
|
|
|
* @method _converse.RosterFilterView#isActive
|
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
isActive () {
|
2020-01-30 12:03:28 +01:00
|
|
|
return (this.model.get('filter_type') === 'state' || this.model.get('filter_text'));
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2017-11-17 11:30:50 +01:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
shouldBeVisible () {
|
2019-08-01 10:26:35 +02:00
|
|
|
return _converse.roster && _converse.roster.length >= 5 || this.isActive();
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2017-11-17 11:30:50 +01:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
clearFilter (ev) {
|
2020-01-30 12:03:28 +01:00
|
|
|
ev && ev.preventDefault();
|
2018-10-23 03:41:38 +02:00
|
|
|
this.model.save({'filter_text': ''});
|
|
|
|
}
|
|
|
|
});
|
2016-04-02 13:30:54 +02:00
|
|
|
|
2018-11-27 15:28:31 +01:00
|
|
|
|
|
|
|
_converse.RosterContactView = _converse.ViewWithAvatar.extend({
|
2018-10-23 03:41:38 +02:00
|
|
|
tagName: 'li',
|
|
|
|
className: 'list-item d-flex hidden controlbox-padded',
|
2016-03-13 17:40:52 +01:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
events: {
|
|
|
|
"click .accept-xmpp-request": "acceptRequest",
|
|
|
|
"click .decline-xmpp-request": "declineRequest",
|
|
|
|
"click .open-chat": "openChat",
|
|
|
|
"click .remove-xmpp-contact": "removeContact"
|
|
|
|
},
|
2016-03-13 17:40:52 +01:00
|
|
|
|
2019-10-24 17:55:20 +02:00
|
|
|
async initialize () {
|
|
|
|
await this.model.initialized;
|
2019-12-18 12:42:40 +01:00
|
|
|
this.debouncedRender = debounce(this.render, 50);
|
|
|
|
this.listenTo(this.model, "change", this.debouncedRender);
|
2019-09-06 14:34:59 +02:00
|
|
|
this.listenTo(this.model, "destroy", this.remove);
|
2019-12-18 12:42:40 +01:00
|
|
|
this.listenTo(this.model, "highlight", this.highlight);
|
2019-09-06 14:34:59 +02:00
|
|
|
this.listenTo(this.model, "open", this.openChat);
|
|
|
|
this.listenTo(this.model, "remove", this.remove);
|
2019-12-18 12:42:40 +01:00
|
|
|
this.listenTo(this.model, 'vcard:change', this.debouncedRender);
|
|
|
|
this.listenTo(this.model.presence, "change:show", this.debouncedRender);
|
2019-10-24 17:55:20 +02:00
|
|
|
this.render();
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
render () {
|
|
|
|
if (!this.mayBeShown()) {
|
|
|
|
u.hideElement(this.el);
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
const ask = this.model.get('ask'),
|
|
|
|
show = this.model.presence.get('show'),
|
|
|
|
requesting = this.model.get('requesting'),
|
2019-12-31 17:05:11 +01:00
|
|
|
subscription = this.model.get('subscription'),
|
|
|
|
jid = this.model.get('jid');
|
2018-10-23 03:41:38 +02:00
|
|
|
|
|
|
|
const classes_to_remove = [
|
|
|
|
'current-xmpp-contact',
|
|
|
|
'pending-xmpp-contact',
|
|
|
|
'requesting-xmpp-contact'
|
2019-04-29 09:29:40 +02:00
|
|
|
].concat(Object.keys(STATUSES));
|
2019-04-29 09:47:46 +02:00
|
|
|
classes_to_remove.forEach(c => u.removeClass(c, this.el));
|
2018-10-23 03:41:38 +02:00
|
|
|
|
|
|
|
this.el.classList.add(show);
|
|
|
|
this.el.setAttribute('data-status', show);
|
|
|
|
this.highlight();
|
|
|
|
|
2018-12-04 12:14:52 +01:00
|
|
|
if (_converse.isUniView()) {
|
2018-10-23 03:41:38 +02:00
|
|
|
const chatbox = _converse.chatboxes.get(this.model.get('jid'));
|
|
|
|
if (chatbox) {
|
|
|
|
if (chatbox.get('hidden')) {
|
|
|
|
this.el.classList.remove('open');
|
|
|
|
} else {
|
|
|
|
this.el.classList.add('open');
|
2018-07-03 21:26:18 +02:00
|
|
|
}
|
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
const display_name = this.model.getDisplayName();
|
|
|
|
this.el.classList.add('pending-xmpp-contact');
|
|
|
|
this.el.innerHTML = tpl_pending_contact(
|
2019-04-29 09:07:15 +02:00
|
|
|
Object.assign(this.model.toJSON(), {
|
2018-11-27 15:28:31 +01:00
|
|
|
display_name,
|
2018-05-05 20:28:41 +02:00
|
|
|
'desc_remove': __('Click to remove %1$s as a contact', display_name),
|
2018-10-23 03:41:38 +02:00
|
|
|
'allow_chat_pending_contacts': _converse.allow_chat_pending_contacts
|
2017-04-20 21:25:58 +02:00
|
|
|
})
|
2017-12-17 16:03:28 +01:00
|
|
|
);
|
2018-10-23 03:41:38 +02:00
|
|
|
} else if (requesting === true) {
|
|
|
|
const display_name = this.model.getDisplayName();
|
|
|
|
this.el.classList.add('requesting-xmpp-contact');
|
|
|
|
this.el.innerHTML = tpl_requesting_contact(
|
2019-04-29 09:07:15 +02:00
|
|
|
Object.assign(this.model.toJSON(), {
|
2018-11-27 15:28:31 +01:00
|
|
|
display_name,
|
2018-10-23 03:41:38 +02:00
|
|
|
'desc_accept': __("Click to accept the contact request from %1$s", display_name),
|
|
|
|
'desc_decline': __("Click to decline the contact request from %1$s", display_name),
|
|
|
|
'allow_chat_pending_contacts': _converse.allow_chat_pending_contacts
|
|
|
|
})
|
|
|
|
);
|
2019-12-31 17:05:11 +01:00
|
|
|
} else if (subscription === 'both' || subscription === 'to' || _converse.rosterview.isSelf(jid)) {
|
2018-10-23 03:41:38 +02:00
|
|
|
this.el.classList.add('current-xmpp-contact');
|
2019-12-17 14:38:12 +01:00
|
|
|
this.el.classList.remove(without(['both', 'to'], subscription)[0]);
|
2018-10-23 03:41:38 +02:00
|
|
|
this.el.classList.add(subscription);
|
|
|
|
this.renderRosterItem(this.model);
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
},
|
2017-04-20 21:25:58 +02:00
|
|
|
|
2019-12-18 11:21:28 +01:00
|
|
|
/**
|
|
|
|
* If appropriate, highlight the contact (by adding the 'open' class).
|
|
|
|
* @private
|
|
|
|
* @method _converse.RosterContactView#highlight
|
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
highlight () {
|
2018-12-04 12:14:52 +01:00
|
|
|
if (_converse.isUniView()) {
|
2018-10-23 03:41:38 +02:00
|
|
|
const chatbox = _converse.chatboxes.get(this.model.get('jid'));
|
2019-03-31 13:05:51 +02:00
|
|
|
if ((chatbox && chatbox.get('hidden')) || !chatbox) {
|
|
|
|
this.el.classList.remove('open');
|
|
|
|
} else {
|
|
|
|
this.el.classList.add('open');
|
2016-04-02 13:30:54 +02:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
},
|
2016-03-13 17:40:52 +01:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
renderRosterItem (item) {
|
|
|
|
const show = item.presence.get('show') || 'offline';
|
2018-11-27 15:28:31 +01:00
|
|
|
let status_icon;
|
2018-10-23 03:41:38 +02:00
|
|
|
if (show === 'online') {
|
|
|
|
status_icon = 'fa fa-circle chat-status chat-status--online';
|
|
|
|
} else if (show === 'away') {
|
|
|
|
status_icon = 'fa fa-circle chat-status chat-status--away';
|
|
|
|
} else if (show === 'xa') {
|
2018-11-27 15:28:31 +01:00
|
|
|
status_icon = 'far fa-circle chat-status chat-status-xa';
|
2018-10-23 03:41:38 +02:00
|
|
|
} else if (show === 'dnd') {
|
|
|
|
status_icon = 'fa fa-minus-circle chat-status chat-status--busy';
|
2018-11-27 15:28:31 +01:00
|
|
|
} else {
|
|
|
|
status_icon = 'fa fa-times-circle chat-status chat-status--offline';
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
const display_name = item.getDisplayName();
|
|
|
|
this.el.innerHTML = tpl_roster_item(
|
2019-04-29 09:07:15 +02:00
|
|
|
Object.assign(item.toJSON(), {
|
2018-11-27 15:28:31 +01:00
|
|
|
show,
|
|
|
|
display_name,
|
|
|
|
status_icon,
|
2018-10-23 03:41:38 +02:00
|
|
|
'desc_status': STATUSES[show],
|
|
|
|
'desc_chat': __('Click to chat with %1$s (JID: %2$s)', display_name, item.get('jid')),
|
|
|
|
'desc_remove': __('Click to remove %1$s as a contact', display_name),
|
|
|
|
'allow_contact_removal': _converse.allow_contact_removal,
|
2018-11-27 15:28:31 +01:00
|
|
|
'num_unread': item.get('num_unread') || 0,
|
|
|
|
classes: ''
|
2018-10-23 03:41:38 +02:00
|
|
|
})
|
|
|
|
);
|
2018-11-27 15:28:31 +01:00
|
|
|
this.renderAvatar();
|
2018-10-23 03:41:38 +02:00
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
2019-10-17 13:17:33 +02:00
|
|
|
/**
|
|
|
|
* Returns 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.
|
|
|
|
* @private
|
|
|
|
* @method _converse.RosterContactView#mayBeShown
|
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
mayBeShown () {
|
|
|
|
const chatStatus = this.model.presence.get('show');
|
2019-10-17 13:17:33 +02:00
|
|
|
if (_converse.hide_offline_users && chatStatus === 'offline') {
|
2018-10-23 03:41:38 +02:00
|
|
|
// If pending or requesting, show
|
|
|
|
if ((this.model.get('ask') === 'subscribe') ||
|
|
|
|
(this.model.get('subscription') === 'from') ||
|
|
|
|
(this.model.get('requesting') === true)) {
|
|
|
|
return true;
|
2016-03-13 17:40:52 +01:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
return false;
|
2016-03-13 17:40:52 +01:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
return true;
|
|
|
|
},
|
2016-03-13 17:40:52 +01:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
openChat (ev) {
|
|
|
|
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
|
|
|
const attrs = this.model.attributes;
|
2019-04-16 16:36:58 +02:00
|
|
|
_converse.api.chats.open(attrs.jid, attrs, true);
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2016-03-13 17:40:52 +01:00
|
|
|
|
2018-11-10 18:53:22 +01:00
|
|
|
async removeContact (ev) {
|
2018-10-23 03:41:38 +02:00
|
|
|
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
|
|
|
if (!_converse.allow_contact_removal) { return; }
|
2018-11-10 18:53:22 +01:00
|
|
|
if (!confirm(__("Are you sure you want to remove this contact?"))) { return; }
|
|
|
|
|
|
|
|
try {
|
2019-10-09 16:01:38 +02:00
|
|
|
await this.model.removeFromRoster();
|
2018-11-10 18:53:22 +01:00
|
|
|
this.remove();
|
|
|
|
if (this.model.collection) {
|
|
|
|
// The model might have already been removed as
|
|
|
|
// result of a roster push.
|
|
|
|
this.model.destroy();
|
|
|
|
}
|
|
|
|
} catch (e) {
|
2019-11-06 11:01:34 +01:00
|
|
|
log.error(e);
|
2019-10-31 14:42:28 +01:00
|
|
|
_converse.api.alert('error', __('Error'),
|
2019-08-07 12:19:53 +02:00
|
|
|
[__('Sorry, there was an error while trying to remove %1$s as a contact.', this.model.getDisplayName())]
|
2019-10-31 14:42:28 +01:00
|
|
|
);
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
},
|
2016-03-13 17:40:52 +01:00
|
|
|
|
2018-11-10 18:53:22 +01:00
|
|
|
async acceptRequest (ev) {
|
2018-10-23 03:41:38 +02:00
|
|
|
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
2018-11-10 18:53:22 +01:00
|
|
|
|
|
|
|
await _converse.roster.sendContactAddIQ(
|
2018-10-23 03:41:38 +02:00
|
|
|
this.model.get('jid'),
|
|
|
|
this.model.getFullname(),
|
2018-11-10 18:53:22 +01:00
|
|
|
[]
|
2018-10-23 03:41:38 +02:00
|
|
|
);
|
2018-11-10 18:53:22 +01:00
|
|
|
this.model.authorize().subscribe();
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2016-03-13 17:40:52 +01:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
declineRequest (ev) {
|
|
|
|
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
|
|
|
const result = confirm(__("Are you sure you want to decline this contact request?"));
|
|
|
|
if (result === true) {
|
|
|
|
this.model.unauthorize().destroy();
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
});
|
2017-12-17 16:03:28 +01:00
|
|
|
|
2019-09-07 22:00:28 +02:00
|
|
|
/**
|
|
|
|
* @class
|
|
|
|
* @namespace _converse.RosterGroupView
|
|
|
|
* @memberOf _converse
|
|
|
|
*/
|
2019-05-23 14:26:20 +02:00
|
|
|
_converse.RosterGroupView = OrderedListView.extend({
|
2018-10-23 03:41:38 +02:00
|
|
|
tagName: 'div',
|
|
|
|
className: 'roster-group hidden',
|
|
|
|
events: {
|
|
|
|
"click a.group-toggle": "toggle"
|
|
|
|
},
|
|
|
|
|
2019-06-27 11:27:05 +02:00
|
|
|
sortImmediatelyOnAdd: true,
|
2018-10-23 03:41:38 +02:00
|
|
|
ItemView: _converse.RosterContactView,
|
|
|
|
listItems: 'model.contacts',
|
|
|
|
listSelector: '.roster-group-contacts',
|
|
|
|
sortEvent: 'presenceChanged',
|
|
|
|
|
|
|
|
initialize () {
|
2019-05-23 14:26:20 +02:00
|
|
|
OrderedListView.prototype.initialize.apply(this, arguments);
|
2019-12-18 11:21:28 +01:00
|
|
|
|
|
|
|
if (this.model.get('name') === _converse.HEADER_UNREAD) {
|
|
|
|
this.listenTo(this.model.contacts, "change:num_unread",
|
|
|
|
c => !this.model.get('unread_messages') && this.removeContact(c)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (this.model.get('name') === _converse.HEADER_REQUESTING_CONTACTS) {
|
|
|
|
this.listenTo(this.model.contacts, "change:requesting",
|
|
|
|
c => !c.get('requesting') && this.removeContact(c)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (this.model.get('name') === _converse.HEADER_PENDING_CONTACTS) {
|
|
|
|
this.listenTo(this.model.contacts, "change:subscription",
|
|
|
|
c => (c.get('subscription') !== 'from') && this.removeContact(c)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-09-06 14:34:59 +02:00
|
|
|
this.listenTo(this.model.contacts, "remove", this.onRemove);
|
|
|
|
this.listenTo(_converse.roster, 'change:groups', this.onContactGroupChange);
|
2018-10-23 03:41:38 +02:00
|
|
|
|
|
|
|
// This event gets triggered once *all* contacts (i.e. not
|
|
|
|
// just this group's) have been fetched from browser
|
|
|
|
// storage or the XMPP server and once they've been
|
|
|
|
// assigned to their various groups.
|
|
|
|
_converse.rosterview.on(
|
|
|
|
'rosterContactsFetchedAndProcessed',
|
2019-06-27 11:27:05 +02:00
|
|
|
() => this.sortAndPositionAllItems()
|
2018-10-23 03:41:38 +02:00
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
render () {
|
|
|
|
this.el.setAttribute('data-group', this.model.get('name'));
|
|
|
|
this.el.innerHTML = tpl_group_header({
|
|
|
|
'label_group': this.model.get('name'),
|
|
|
|
'desc_group_toggle': this.model.get('description'),
|
|
|
|
'toggle_state': this.model.get('state'),
|
|
|
|
'_converse': _converse
|
|
|
|
});
|
|
|
|
this.contacts_el = this.el.querySelector('.roster-group-contacts');
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
show () {
|
|
|
|
u.showElement(this.el);
|
2019-04-29 09:47:46 +02:00
|
|
|
if (this.model.get('state') === _converse.OPENED) {
|
|
|
|
Object.values(this.getAll())
|
|
|
|
.filter(v => v.mayBeShown())
|
|
|
|
.forEach(v => u.showElement(v.el));
|
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
return this;
|
|
|
|
},
|
2016-03-13 17:40:52 +01:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
collapse () {
|
|
|
|
return u.slideIn(this.contacts_el);
|
|
|
|
},
|
|
|
|
|
2019-10-17 13:28:18 +02:00
|
|
|
/* Given a list of contacts, make sure they're filtered out
|
|
|
|
* (aka hidden) and that all other contacts are visible.
|
|
|
|
* If all contacts are hidden, then also hide the group title.
|
|
|
|
* @private
|
|
|
|
* @method _converse.RosterGroupView#filterOutContacts
|
|
|
|
* @param { Array } contacts
|
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
filterOutContacts (contacts=[]) {
|
|
|
|
let shown = 0;
|
2019-06-27 11:27:05 +02:00
|
|
|
this.model.contacts.forEach(contact => {
|
2018-10-23 03:41:38 +02:00
|
|
|
const contact_view = this.get(contact.get('id'));
|
2019-12-17 14:27:56 +01:00
|
|
|
if (contacts.includes(contact)) {
|
2018-10-23 03:41:38 +02:00
|
|
|
u.hideElement(contact_view.el);
|
|
|
|
} else if (contact_view.mayBeShown()) {
|
|
|
|
u.showElement(contact_view.el);
|
|
|
|
shown += 1;
|
2018-02-19 16:24:05 +01:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
});
|
|
|
|
if (shown) {
|
|
|
|
u.showElement(this.el);
|
|
|
|
} else {
|
|
|
|
u.hideElement(this.el);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2019-10-17 13:28:18 +02:00
|
|
|
/**
|
|
|
|
* Given the filter query "q" and the filter type "type",
|
|
|
|
* return a list of contacts that need to be filtered out.
|
|
|
|
* @private
|
|
|
|
* @method _converse.RosterGroupView#getFilterMatches
|
|
|
|
* @param { String } q - The filter query
|
|
|
|
* @param { String } type - The filter type
|
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
getFilterMatches (q, type) {
|
|
|
|
if (q.length === 0) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
let matches;
|
|
|
|
q = q.toLowerCase();
|
|
|
|
if (type === 'state') {
|
2019-12-18 11:21:28 +01:00
|
|
|
const sticky_groups = [_converse.HEADER_REQUESTING_CONTACTS, _converse.HEADER_UNREAD];
|
|
|
|
if (sticky_groups.includes(this.model.get('name'))) {
|
2018-10-23 03:41:38 +02:00
|
|
|
// When filtering by chat state, we still want to
|
2019-12-18 11:21:28 +01:00
|
|
|
// show sticky groups, even though they don't
|
|
|
|
// match the state in question.
|
|
|
|
return [];
|
2018-10-23 03:41:38 +02:00
|
|
|
} else if (q === 'unread_messages') {
|
|
|
|
matches = this.model.contacts.filter({'num_unread': 0});
|
2019-10-17 13:28:18 +02:00
|
|
|
} else if (q === 'online') {
|
|
|
|
matches = this.model.contacts.filter(c => ["offline", "unavailable"].includes(c.presence.get('show')));
|
2016-03-13 17:40:52 +01:00
|
|
|
} else {
|
2019-12-17 14:27:56 +01:00
|
|
|
matches = this.model.contacts.filter(c => !c.presence.get('show').includes(q));
|
2016-03-13 17:40:52 +01:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
} else {
|
|
|
|
matches = this.model.contacts.filter((contact) => {
|
2019-12-17 14:27:56 +01:00
|
|
|
return !contact.getDisplayName().toLowerCase().includes(q.toLowerCase());
|
2018-10-23 03:41:38 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
return matches;
|
|
|
|
},
|
2016-03-13 17:40:52 +01:00
|
|
|
|
2019-08-05 01:39:57 +02:00
|
|
|
/**
|
|
|
|
* Filter the group's contacts based on the query "q".
|
|
|
|
*
|
|
|
|
* If all contacts are filtered out (i.e. hidden), then the
|
|
|
|
* group must be filtered out as well.
|
|
|
|
* @private
|
2019-09-07 22:00:28 +02:00
|
|
|
* @method _converse.RosterGroupView#filter
|
2019-08-05 01:39:57 +02:00
|
|
|
* @param { string } q - The query to filter against
|
|
|
|
* @param { string } type
|
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
filter (q, type) {
|
2019-08-05 01:39:57 +02:00
|
|
|
if (q === null || q === undefined) {
|
2018-10-23 03:41:38 +02:00
|
|
|
type = type || _converse.rosterview.filter_view.model.get('filter_type');
|
|
|
|
if (type === 'state') {
|
|
|
|
q = _converse.rosterview.filter_view.model.get('chat_state');
|
|
|
|
} else {
|
|
|
|
q = _converse.rosterview.filter_view.model.get('filter_text');
|
2016-03-13 17:40:52 +01:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
this.filterOutContacts(this.getFilterMatches(q, type));
|
|
|
|
},
|
2016-03-13 17:40:52 +01:00
|
|
|
|
2018-10-24 18:28:28 +02:00
|
|
|
async toggle (ev) {
|
2018-10-23 03:41:38 +02:00
|
|
|
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
2019-01-03 11:45:52 +01:00
|
|
|
const icon_el = ev.target.matches('.fa') ? ev.target : ev.target.querySelector('.fa');
|
2019-12-17 14:27:56 +01:00
|
|
|
if (u.hasClass("fa-caret-down", icon_el)) {
|
2018-10-23 03:41:38 +02:00
|
|
|
this.model.save({state: _converse.CLOSED});
|
2018-10-24 18:28:28 +02:00
|
|
|
await this.collapse();
|
|
|
|
icon_el.classList.remove("fa-caret-down");
|
|
|
|
icon_el.classList.add("fa-caret-right");
|
2018-10-23 03:41:38 +02:00
|
|
|
} else {
|
|
|
|
icon_el.classList.remove("fa-caret-right");
|
|
|
|
icon_el.classList.add("fa-caret-down");
|
|
|
|
this.model.save({state: _converse.OPENED});
|
|
|
|
this.filter();
|
|
|
|
u.showElement(this.el);
|
|
|
|
u.slideOut(this.contacts_el);
|
2016-03-13 17:40:52 +01:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2016-11-02 14:13:49 +01:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
onContactGroupChange (contact) {
|
2019-12-17 14:27:56 +01:00
|
|
|
const in_this_group = contact.get('groups').includes(this.model.get('name'));
|
2018-10-23 03:41:38 +02:00
|
|
|
const cid = contact.get('id');
|
|
|
|
const in_this_overview = !this.get(cid);
|
|
|
|
if (in_this_group && !in_this_overview) {
|
|
|
|
this.items.trigger('add', contact);
|
|
|
|
} else if (!in_this_group) {
|
|
|
|
this.removeContact(contact);
|
|
|
|
}
|
|
|
|
},
|
2017-12-22 15:40:58 +01:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
removeContact (contact) {
|
|
|
|
// We suppress events, otherwise the remove event will
|
|
|
|
// also cause the contact's view to be removed from the
|
|
|
|
// "Pending Contacts" group.
|
|
|
|
this.model.contacts.remove(contact, {'silent': true});
|
|
|
|
this.onRemove(contact);
|
|
|
|
},
|
2017-12-22 15:42:06 +01:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
onRemove (contact) {
|
|
|
|
this.remove(contact.get('jid'));
|
|
|
|
if (this.model.contacts.length === 0) {
|
|
|
|
this.remove();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2017-12-22 15:40:58 +01:00
|
|
|
|
|
|
|
|
2019-09-07 22:00:28 +02:00
|
|
|
/**
|
|
|
|
* @class
|
|
|
|
* @namespace _converse.RosterView
|
|
|
|
* @memberOf _converse
|
|
|
|
*/
|
2019-05-23 14:26:20 +02:00
|
|
|
_converse.RosterView = OrderedListView.extend({
|
2018-10-23 03:41:38 +02:00
|
|
|
tagName: 'div',
|
|
|
|
id: 'converse-roster',
|
|
|
|
className: 'controlbox-section',
|
2017-12-22 15:40:58 +01:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
ItemView: _converse.RosterGroupView,
|
|
|
|
listItems: 'model',
|
|
|
|
listSelector: '.roster-contacts',
|
|
|
|
sortEvent: null, // Groups are immutable, so they don't get re-sorted
|
|
|
|
subviewIndex: 'name',
|
2019-06-27 11:27:05 +02:00
|
|
|
sortImmediatelyOnAdd: true,
|
2017-12-22 15:40:58 +01:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
events: {
|
2018-11-07 11:09:41 +01:00
|
|
|
'click a.controlbox-heading__btn.add-contact': 'showAddContactModal',
|
2018-11-07 11:12:04 +01:00
|
|
|
'click a.controlbox-heading__btn.sync-contacts': 'syncContacts'
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
initialize () {
|
2019-05-23 14:26:20 +02:00
|
|
|
OrderedListView.prototype.initialize.apply(this, arguments);
|
2017-12-22 15:40:58 +01:00
|
|
|
|
2019-09-06 14:34:59 +02:00
|
|
|
this.listenTo(_converse.roster, "add", this.onContactAdded);
|
|
|
|
this.listenTo(_converse.roster, 'change:groups', this.onContactAdded);
|
|
|
|
this.listenTo(_converse.roster, 'change', this.onContactChange);
|
|
|
|
this.listenTo(_converse.roster, "destroy", this.update);
|
|
|
|
this.listenTo(_converse.roster, "remove", this.update);
|
2018-10-23 03:41:38 +02:00
|
|
|
_converse.presences.on('change:show', () => {
|
2018-05-22 16:44:58 +02:00
|
|
|
this.update();
|
2017-12-22 15:40:58 +01:00
|
|
|
this.updateFilter();
|
2018-10-23 03:41:38 +02:00
|
|
|
});
|
|
|
|
|
2019-09-06 14:34:59 +02:00
|
|
|
this.listenTo(this.model, "reset", this.reset);
|
2018-10-23 03:41:38 +02:00
|
|
|
|
|
|
|
// This event gets triggered once *all* contacts (i.e. not
|
|
|
|
// just this group's) have been fetched from browser
|
|
|
|
// storage or the XMPP server and once they've been
|
|
|
|
// assigned to their various groups.
|
2019-03-29 14:15:03 +01:00
|
|
|
_converse.api.listen.on('rosterGroupsFetched', this.sortAndPositionAllItems.bind(this));
|
2017-12-22 15:40:58 +01:00
|
|
|
|
2019-03-29 14:15:03 +01:00
|
|
|
_converse.api.listen.on('rosterContactsFetched', () => {
|
2019-06-27 11:27:05 +02:00
|
|
|
_converse.roster.each(contact => this.addRosterContact(contact, {'silent': true}));
|
2018-05-22 16:44:58 +02:00
|
|
|
this.update();
|
2017-12-22 15:40:58 +01:00
|
|
|
this.updateFilter();
|
2018-10-23 03:41:38 +02:00
|
|
|
this.trigger('rosterContactsFetchedAndProcessed');
|
|
|
|
});
|
|
|
|
this.createRosterFilter();
|
|
|
|
},
|
2017-12-22 15:40:58 +01:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
render () {
|
|
|
|
this.el.innerHTML = tpl_roster({
|
|
|
|
'allow_contact_requests': _converse.allow_contact_requests,
|
|
|
|
'heading_contacts': __('Contacts'),
|
2018-11-07 11:12:04 +01:00
|
|
|
'title_add_contact': __('Add a contact'),
|
|
|
|
'title_sync_contacts': __('Re-sync your contacts')
|
2018-10-23 03:41:38 +02:00
|
|
|
});
|
|
|
|
const form = this.el.querySelector('.roster-filter-form');
|
|
|
|
this.el.replaceChild(this.filter_view.render().el, form);
|
|
|
|
this.roster_el = this.el.querySelector('.roster-contacts');
|
|
|
|
return this;
|
|
|
|
},
|
2017-12-22 15:40:58 +01:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
showAddContactModal (ev) {
|
2019-07-29 10:19:05 +02:00
|
|
|
if (this.add_contact_modal === undefined) {
|
2019-09-19 16:54:55 +02:00
|
|
|
this.add_contact_modal = new _converse.AddContactModal({'model': new Model()});
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
this.add_contact_modal.show(ev);
|
|
|
|
},
|
|
|
|
|
|
|
|
createRosterFilter () {
|
|
|
|
// Create a model on which we can store filter properties
|
|
|
|
const model = new _converse.RosterFilter();
|
2019-10-24 14:29:15 +02:00
|
|
|
model.id = `_converse.rosterfilter-${_converse.bare_jid}`;
|
|
|
|
model.browserStorage = _converse.createStore(model.id);
|
2020-01-30 12:03:28 +01:00
|
|
|
this.filter_view = new _converse.RosterFilterView({model});
|
2019-09-06 14:34:59 +02:00
|
|
|
this.listenTo(this.filter_view.model, 'change', this.updateFilter);
|
2018-10-23 03:41:38 +02:00
|
|
|
this.filter_view.model.fetch();
|
|
|
|
},
|
|
|
|
|
2019-12-17 14:38:12 +01:00
|
|
|
updateFilter: debounce(function () {
|
2018-10-23 03:41:38 +02:00
|
|
|
/* 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.
|
|
|
|
*/
|
|
|
|
const 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),
|
|
|
|
|
2019-05-23 21:44:00 +02:00
|
|
|
update () {
|
2018-10-23 03:41:38 +02:00
|
|
|
if (!u.isVisible(this.roster_el)) {
|
|
|
|
u.showElement(this.roster_el);
|
|
|
|
}
|
2020-01-30 12:03:28 +01:00
|
|
|
this.filter_view.render();
|
2018-10-23 03:41:38 +02:00
|
|
|
return this;
|
2019-05-23 21:44:00 +02:00
|
|
|
},
|
2018-10-23 03:41:38 +02:00
|
|
|
|
|
|
|
filter (query, type) {
|
2019-09-07 22:00:28 +02:00
|
|
|
const views = Object.values(this.getAll());
|
|
|
|
// First ensure the filter is restored to its original state
|
|
|
|
views.forEach(v => (v.model.contacts.length > 0) && v.show().filter(''));
|
2018-10-23 03:41:38 +02:00
|
|
|
// Now we can filter
|
|
|
|
query = query.toLowerCase();
|
|
|
|
if (type === 'groups') {
|
2019-09-07 22:00:28 +02:00
|
|
|
views.forEach(view => {
|
|
|
|
if (!view.model.get('name').toLowerCase().includes(query)) {
|
2018-10-23 03:41:38 +02:00
|
|
|
u.slideIn(view.el);
|
|
|
|
} else if (view.model.contacts.length > 0) {
|
|
|
|
u.slideOut(view.el);
|
2017-12-22 15:40:58 +01:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
});
|
|
|
|
} else {
|
2019-09-07 22:00:28 +02:00
|
|
|
views.forEach(v => v.filter(query, type));
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
},
|
2017-12-22 15:40:58 +01:00
|
|
|
|
2018-11-07 11:12:04 +01:00
|
|
|
async syncContacts (ev) {
|
|
|
|
ev.preventDefault();
|
|
|
|
u.addClass('fa-spin', ev.target);
|
|
|
|
_converse.roster.data.save('version', null);
|
|
|
|
await _converse.roster.fetchFromServer();
|
|
|
|
_converse.xmppstatus.sendPresence();
|
|
|
|
u.removeClass('fa-spin', ev.target);
|
|
|
|
},
|
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
reset () {
|
|
|
|
this.removeAll();
|
|
|
|
this.render().update();
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
onContactAdded (contact) {
|
|
|
|
this.addRosterContact(contact)
|
|
|
|
this.update();
|
|
|
|
this.updateFilter();
|
|
|
|
},
|
|
|
|
|
|
|
|
onContactChange (contact) {
|
|
|
|
this.update();
|
2019-12-17 14:38:12 +01:00
|
|
|
if (has(contact.changed, 'subscription')) {
|
2018-10-23 03:41:38 +02:00
|
|
|
if (contact.changed.subscription === 'from') {
|
2019-05-24 13:52:15 +02:00
|
|
|
this.addContactToGroup(contact, _converse.HEADER_PENDING_CONTACTS);
|
2019-12-17 14:27:56 +01:00
|
|
|
} else if (['both', 'to'].includes(contact.get('subscription'))) {
|
2018-10-23 03:41:38 +02:00
|
|
|
this.addExistingContact(contact);
|
2017-12-22 15:40:58 +01:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
2019-12-18 11:21:28 +01:00
|
|
|
if (has(contact.changed, 'num_unread') && contact.get('num_unread')) {
|
|
|
|
this.addContactToGroup(contact, _converse.HEADER_UNREAD);
|
|
|
|
}
|
2019-12-17 14:38:12 +01:00
|
|
|
if (has(contact.changed, 'ask') && contact.changed.ask === 'subscribe') {
|
2019-05-24 13:52:15 +02:00
|
|
|
this.addContactToGroup(contact, _converse.HEADER_PENDING_CONTACTS);
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
2019-12-17 14:38:12 +01:00
|
|
|
if (has(contact.changed, 'subscription') && contact.changed.requesting === 'true') {
|
2019-05-24 13:52:15 +02:00
|
|
|
this.addContactToGroup(contact, _converse.HEADER_REQUESTING_CONTACTS);
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
this.updateFilter();
|
|
|
|
},
|
|
|
|
|
2019-09-07 22:00:28 +02:00
|
|
|
/**
|
|
|
|
* Returns the group as specified by name.
|
|
|
|
* Creates the group if it doesn't exist.
|
|
|
|
* @method _converse.RosterView#getGroup
|
|
|
|
* @private
|
|
|
|
* @param {string} name
|
|
|
|
*/
|
2018-10-23 03:41:38 +02:00
|
|
|
getGroup (name) {
|
|
|
|
const view = this.get(name);
|
|
|
|
if (view) {
|
|
|
|
return view.model;
|
|
|
|
}
|
2020-01-30 12:56:56 +01:00
|
|
|
return this.model.create({name});
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2017-12-22 15:40:58 +01:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
addContactToGroup (contact, name, options) {
|
|
|
|
this.getGroup(name).contacts.add(contact, options);
|
|
|
|
this.sortAndPositionAllItems();
|
|
|
|
},
|
2018-07-03 21:26:18 +02:00
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
addExistingContact (contact, options) {
|
|
|
|
let groups;
|
|
|
|
if (_converse.roster_groups) {
|
|
|
|
groups = contact.get('groups');
|
2019-09-07 22:00:28 +02:00
|
|
|
groups = (groups.length === 0) ? [_converse.HEADER_UNGROUPED] : groups;
|
2018-10-23 03:41:38 +02:00
|
|
|
} else {
|
2019-05-24 13:52:15 +02:00
|
|
|
groups = [_converse.HEADER_CURRENT_CONTACTS];
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
2019-12-18 11:21:28 +01:00
|
|
|
if (contact.get('num_unread')) {
|
2019-12-18 12:06:32 +01:00
|
|
|
groups.push(_converse.HEADER_UNREAD);
|
2019-12-18 11:21:28 +01:00
|
|
|
}
|
2019-09-07 22:00:28 +02:00
|
|
|
groups.forEach(g => this.addContactToGroup(contact, g, options));
|
2018-10-23 03:41:38 +02:00
|
|
|
},
|
2017-05-03 09:06:28 +02:00
|
|
|
|
2019-12-31 17:05:11 +01:00
|
|
|
isSelf (jid) {
|
|
|
|
return u.isSameBareJID(jid, _converse.connection.jid);
|
|
|
|
},
|
|
|
|
|
2018-10-23 03:41:38 +02:00
|
|
|
addRosterContact (contact, options) {
|
2019-12-31 17:05:11 +01:00
|
|
|
const jid = contact.get('jid');
|
|
|
|
if (contact.get('subscription') === 'both' || contact.get('subscription') === 'to' || this.isSelf(jid)) {
|
2018-10-23 03:41:38 +02:00
|
|
|
this.addExistingContact(contact, options);
|
|
|
|
} else {
|
|
|
|
if (!_converse.allow_contact_requests) {
|
2019-11-06 11:01:34 +01:00
|
|
|
log.debug(
|
2019-12-31 17:05:11 +01:00
|
|
|
`Not adding requesting or pending contact ${jid} `+
|
2019-11-06 11:01:34 +01:00
|
|
|
`because allow_contact_requests is false`
|
2018-10-23 03:41:38 +02:00
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if ((contact.get('ask') === 'subscribe') || (contact.get('subscription') === 'from')) {
|
2019-05-24 13:52:15 +02:00
|
|
|
this.addContactToGroup(contact, _converse.HEADER_PENDING_CONTACTS, options);
|
2018-10-23 03:41:38 +02:00
|
|
|
} else if (contact.get('requesting') === true) {
|
2019-05-24 13:52:15 +02:00
|
|
|
this.addContactToGroup(contact, _converse.HEADER_REQUESTING_CONTACTS, options);
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
2018-07-16 00:09:54 +02:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
return this;
|
2018-06-07 13:40:20 +02:00
|
|
|
}
|
2018-10-23 03:41:38 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
/* -------- Event Handlers ----------- */
|
|
|
|
_converse.api.listen.on('chatBoxesInitialized', () => {
|
2019-03-31 13:05:51 +02:00
|
|
|
function highlightRosterItem (chatbox) {
|
2019-08-01 10:26:35 +02:00
|
|
|
const contact = _converse.roster && _converse.roster.findWhere({'jid': chatbox.get('jid')});
|
2019-07-29 10:19:05 +02:00
|
|
|
if (contact !== undefined) {
|
2019-03-31 13:05:51 +02:00
|
|
|
contact.trigger('highlight');
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
2019-03-31 13:05:51 +02:00
|
|
|
}
|
|
|
|
_converse.chatboxes.on('destroy', chatbox => highlightRosterItem(chatbox));
|
|
|
|
_converse.chatboxes.on('change:hidden', chatbox => highlightRosterItem(chatbox));
|
2018-10-23 03:41:38 +02:00
|
|
|
});
|
|
|
|
|
2019-08-02 08:41:48 +02:00
|
|
|
|
2019-08-05 11:10:32 +02:00
|
|
|
_converse.api.listen.on('controlBoxInitialized', (view) => {
|
2019-08-02 08:41:48 +02:00
|
|
|
function insertRoster () {
|
|
|
|
if (!view.model.get('connected') || _converse.authentication === _converse.ANONYMOUS) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
/* Place the rosterview inside the "Contacts" panel. */
|
|
|
|
_converse.api.waitUntil('rosterViewInitialized')
|
|
|
|
.then(() => view.controlbox_pane.el.insertAdjacentElement('beforeEnd', _converse.rosterview.el))
|
2019-11-06 11:01:34 +01:00
|
|
|
.catch(e => log.fatal(e));
|
2019-08-02 08:41:48 +02:00
|
|
|
}
|
|
|
|
insertRoster();
|
|
|
|
view.model.on('change:connected', insertRoster);
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2019-10-24 17:55:20 +02:00
|
|
|
function initRosterView () {
|
2018-10-23 03:41:38 +02:00
|
|
|
/* Create an instance of RosterView once the RosterGroups
|
|
|
|
* collection has been created (in @converse/headless/converse-core.js)
|
|
|
|
*/
|
|
|
|
if (_converse.authentication === _converse.ANONYMOUS) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
_converse.rosterview = new _converse.RosterView({
|
|
|
|
'model': _converse.rostergroups
|
|
|
|
});
|
2019-03-29 21:36:49 +01:00
|
|
|
_converse.rosterview.render();
|
|
|
|
/**
|
|
|
|
* Triggered once the _converse.RosterView instance has been created and initialized.
|
|
|
|
* @event _converse#rosterViewInitialized
|
|
|
|
* @example _converse.api.listen.on('rosterViewInitialized', () => { ... });
|
|
|
|
*/
|
|
|
|
_converse.api.trigger('rosterViewInitialized');
|
2016-03-13 17:40:52 +01:00
|
|
|
}
|
2019-10-24 17:55:20 +02:00
|
|
|
_converse.api.listen.on('rosterInitialized', initRosterView);
|
|
|
|
_converse.api.listen.on('rosterReadyAfterReconnection', initRosterView);
|
2019-02-20 22:58:59 +01:00
|
|
|
|
|
|
|
_converse.api.listen.on('afterTearDown', () => {
|
|
|
|
if (converse.rosterview) {
|
|
|
|
converse.rosterview.model.off().reset();
|
|
|
|
converse.rosterview.each(groupview => groupview.removeAll().remove());
|
|
|
|
converse.rosterview.removeAll().remove();
|
|
|
|
delete converse.rosterview;
|
|
|
|
}
|
|
|
|
});
|
2018-10-23 03:41:38 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|