Commit cc865de0 authored by JC Brand's avatar JC Brand

Add new config setting `autocomplete_add_contact`

Determines whether search suggestions are shown in the "Add Contact" modal.
parent 42128e05
# Changelog
## 4.1.3 (Unreleased)
## 4.2.0 (Unreleased)
- Updated translation: lt
- Upgrade to Backbone 1.4.0, Strophe 1.3.2 and Jasmine 2.99.2
......@@ -9,13 +9,14 @@
- Fix handling of CAPTCHAs offered by ejabberd
- Don't send out receipts or markers for MAM messages
- Allow setting of debug mode via URL with `/#converse?debug=true`
- Render inline images served over HTTP if Converse itself was loaded on an unsecured (HTTP) page.
- Make sure `nickname` passed in via `_converse.initialize` has first preference as MUC nickname
- Make sure required registration fields have "required" attribute
- New config setting [autocomplete_add_contact](https://conversejs.org/docs/html/configuration.html#autocomplete-add-contact)
- New config setting [locked_muc_domain](https://conversejs.org/docs/html/configuration.html#locked-muc-domain)
- New config setting [locked_muc_nickname](https://conversejs.org/docs/html/configuration.html#locked-muc-nickname)
- New config setting [show_client_info](https://conversejs.org/docs/html/configuration.html#show-client-info)
- Render inline images served over HTTP if Converse itself was loaded on an unsecured (HTTP) page.
- Document new API method [sendMessage](https://conversejs.org/docs/html/api/-_converse.ChatBox.html#sendMessage)
- Make sure `nickname` passed in via `_converse.initialize` has first preference as MUC nickname
- Make sure required registration fields have "required" attribute
- #1149: With `xhr_user_search_url`, contact requests are not being sent out
- #1213: Switch roster filter input and icons
- #1327: fix False mentions positives in URLs and Email addresses
......
......@@ -58987,6 +58987,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_4__["default"].plugins
__ = _converse.__;
_converse.api.settings.update({
'autocomplete_add_contact': true,
'allow_chat_pending_contacts': true,
'allow_contact_removal': true,
'hide_offline_users': false,
......@@ -59075,10 +59076,6 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_4__["default"].plugins
afterRender() {
if (_converse.xhr_user_search_url && _.isString(_converse.xhr_user_search_url)) {
this.initXHRAutoComplete();
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;
});
} else {
this.initJIDAutoComplete();
}
......@@ -59088,6 +59085,10 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_4__["default"].plugins
},
initJIDAutoComplete() {
if (!_converse.autocomplete_add_contact) {
return;
}
const el = this.el.querySelector('.suggestion-box__jid').parentElement;
this.jid_auto_complete = new _converse.AutoComplete(el, {
'data': (text, input) => `${input.slice(0, input.indexOf("@"))}@${text}`,
......@@ -59097,6 +59098,10 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_4__["default"].plugins
},
initXHRAutoComplete() {
if (!_converse.autocomplete_add_contact) {
return this.initXHRFetch();
}
const el = this.el.querySelector('.suggestion-box__name').parentElement;
this.name_auto_complete = new _converse.AutoComplete(el, {
'auto_evaluate': false,
......@@ -59122,28 +59127,76 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_4__["default"].plugins
xhr.open("GET", `${_converse.xhr_user_search_url}q=${input_el.value}`, true);
xhr.send();
}, 300));
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;
});
},
addContactFromForm(ev) {
ev.preventDefault();
const data = new FormData(ev.target),
jid = data.get('jid'),
name = data.get('name');
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) {
const el = this.el.querySelector('.suggestion-box__name .invalid-feedback');
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) {
if (!jid || _.compact(jid.split('@')).length < 2) {
// XXX: we used to have to do this manually, instead of via
// toHTML because Awesomplete messes things up and
// confuses Snabbdom
// We now use _converse.AutoComplete, can this be removed?
u.addClass('is-invalid', this.el.querySelector('input[name="jid"]'));
u.addClass('d-block', this.el.querySelector('.invalid-feedback'));
} else {
ev.target.reset();
u.addClass('d-block', this.el.querySelector('.suggestion-box__jid .invalid-feedback'));
return false;
}
_converse.roster.addAndSubscribe(jid, name);
return true;
},
this.model.clear();
this.modal.hide();
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),
jid = data.get('jid');
if (!jid && _converse.xhr_user_search_url && _.isString(_converse.xhr_user_search_url)) {
const input_el = this.el.querySelector('input[name="name"]');
this.xhr.open("GET", `${_converse.xhr_user_search_url}q=${input_el.value}`, true);
this.xhr.send();
return;
}
if (this.validateSubmission(jid)) {
this.afterSubmission(ev.target, jid, data.get('name'));
}
}
......@@ -68597,7 +68650,7 @@ _converse_headless_converse_core__WEBPACK_IMPORTED_MODULE_0__["default"].plugins
* (Function) callback - A function to call once the IQ is returned
* (Function) errback - A function to call if an error occurred
*/
name = _.isEmpty(name) ? jid : name;
name = _.isEmpty(name) ? null : name;
const iq = $iq({
'type': 'set'
}).c('query', {
......@@ -92115,7 +92168,11 @@ __p += ' hidden ';
} ;
__p += '">\n <label class="clearfix" for="jid">' +
__e(o.label_xmpp_address) +
':</label>\n <div class="suggestion-box suggestion-box__jid">\n <ul class="suggestion-box__results suggestion-box__results--above" hidden=""></ul>\n <input type="text" name="jid" required="required" value="' +
':</label>\n <div class="suggestion-box suggestion-box__jid">\n <ul class="suggestion-box__results suggestion-box__results--above" hidden=""></ul>\n <input type="text" name="jid"\n ';
if (!o._converse.xhr_user_search_url) { ;
__p += ' required="required" ';
} ;
__p += '\n value="' +
__e(o.jid) +
'"\n class="form-control suggestion-box__input"\n placeholder="' +
__e(o.contact_placeholder) +
......@@ -250,6 +250,13 @@ available) and the amount returned will be no more than the page size.
You will be able to query for even older messages by scrolling upwards in the chatbox or room
(the so-called infinite scrolling pattern).
autocomplete_add_contact
------------------------
* Default: ``true``
Determines whether search suggestions are shown in the "Add Contact" modal.
auto_list_rooms
---------------
......
......@@ -218,13 +218,44 @@
done();
}));
it("can be configured to not provide search suggestions",
mock.initConverse(
null, ['rosterGroupsFetched'], {'autocomplete_add_contact': false},
async function (done, _converse) {
test_utils.openControlBox();
const panel = _converse.chatboxviews.get('controlbox').contactspanel;
const cbview = _converse.chatboxviews.get('controlbox');
cbview.el.querySelector('.add-contact').click()
const modal = _converse.rosterview.add_contact_modal;
expect(modal.jid_auto_complete).toBe(undefined);
expect(modal.name_auto_complete).toBe(undefined);
await test_utils.waitUntil(() => u.isVisible(modal.el), 1000);
const sendIQ = _converse.connection.sendIQ;
let sent_stanza;
spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
sent_stanza = iq;
sendIQ.bind(this)(iq, callback, errback);
});
expect(!_.isNull(modal.el.querySelector('form.add-xmpp-contact'))).toBeTruthy();
const input_jid = modal.el.querySelector('input[name="jid"]');
const input_name = modal.el.querySelector('input[name="name"]');
input_jid.value = 'someone@localhost';
modal.el.querySelector('button[type="submit"]').click();
await test_utils.waitUntil(() => sent_stanza.nodeTree.matches('iq'));
expect(sent_stanza.toLocaleString()).toEqual(
`<iq id="${sent_stanza.nodeTree.getAttribute('id')}" type="set" xmlns="jabber:client">`+
`<query xmlns="jabber:iq:roster"><item jid="someone@localhost"/></query>`+
`</iq>`);
done();
}));
it("integrates with xhr_user_search_url to search for contacts",
mock.initConverse(
null, ['rosterGroupsFetched'],
{ 'xhr_user_search': true,
'xhr_user_search_url': 'http://example.org/?'
},
{ 'xhr_user_search_url': 'http://example.org/?' },
async function (done, _converse) {
const xhr = {
......@@ -278,5 +309,74 @@
window.XMLHttpRequest = XMLHttpRequestBackup;
done();
}));
it("can be configured to not provide search suggestions for XHR search results",
mock.initConverse(
null, ['rosterGroupsFetched'],
{ 'autocomplete_add_contact': false,
'xhr_user_search_url': 'http://example.org/?' },
async function (done, _converse) {
var modal;
const xhr = {
'open': _.noop,
'send': function () {
const value = modal.el.querySelector('input[name="name"]').value;
if (value === 'ambiguous') {
xhr.responseText = JSON.stringify([
{"jid": "marty@mcfly.net", "fullname": "Marty McFly"},
{"jid": "doc@brown.com", "fullname": "Doc Brown"}
]);
} else if (value === 'insufficient') {
xhr.responseText = JSON.stringify([]);
} else {
xhr.responseText = JSON.stringify([{"jid": "marty@mcfly.net", "fullname": "Marty McFly"}]);
}
xhr.onload();
}
};
const XMLHttpRequestBackup = window.XMLHttpRequest;
window.XMLHttpRequest = jasmine.createSpy('XMLHttpRequest');
XMLHttpRequest.and.callFake(() => xhr);
const panel = _converse.chatboxviews.get('controlbox').contactspanel;
const cbview = _converse.chatboxviews.get('controlbox');
cbview.el.querySelector('.add-contact').click()
modal = _converse.rosterview.add_contact_modal;
await test_utils.waitUntil(() => u.isVisible(modal.el), 1000);
expect(modal.jid_auto_complete).toBe(undefined);
expect(modal.name_auto_complete).toBe(undefined);
const sendIQ = _converse.connection.sendIQ;
let sent_stanza, IQ_id;
spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
sent_stanza = iq;
IQ_id = sendIQ.bind(this)(iq, callback, errback);
});
const input_el = modal.el.querySelector('input[name="name"]');
input_el.value = 'ambiguous';
modal.el.querySelector('button[type="submit"]').click();
let feedback_el = modal.el.querySelector('.suggestion-box__name .invalid-feedback');
expect(feedback_el.textContent).toBe('Sorry, could not find a contact with that name');
feedback_el.textContent = '';
input_el.value = 'insufficient';
modal.el.querySelector('button[type="submit"]').click();
feedback_el = modal.el.querySelector('.suggestion-box__name .invalid-feedback');
expect(feedback_el.textContent).toBe('Sorry, could not find a contact with that name');
input_el.value = 'Marty McFly';
modal.el.querySelector('button[type="submit"]').click();
expect(sent_stanza.toLocaleString()).toEqual(
`<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
`<query xmlns="jabber:iq:roster"><item jid="marty@mcfly.net" name="Marty McFly"/></query>`+
`</iq>`);
window.XMLHttpRequest = XMLHttpRequestBackup;
done();
}));
});
}));
......@@ -117,7 +117,7 @@
expect(sent_stanza.toLocaleString()).toBe(
`<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
`<query xmlns="jabber:iq:roster">`+
`<item jid="contact@example.org" name="contact@example.org"/>`+
`<item jid="contact@example.org"/>`+
`</query>`+
`</iq>`
);
......
......@@ -55,13 +55,14 @@ converse.plugins.add('converse-rosterview', {
{ __ } = _converse;
_converse.api.settings.update({
'autocomplete_add_contact': true,
'allow_chat_pending_contacts': true,
'allow_contact_removal': true,
'hide_offline_users': false,
'roster_groups': true,
'show_only_online_users': false,
'show_toolbar': true,
'xhr_user_search_url': null
'xhr_user_search_url': null,
});
_converse.api.promises.add('rosterViewInitialized');
......@@ -132,10 +133,6 @@ converse.plugins.add('converse-rosterview', {
afterRender () {
if (_converse.xhr_user_search_url && _.isString(_converse.xhr_user_search_url)) {
this.initXHRAutoComplete();
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;
});
} else {
this.initJIDAutoComplete();
}
......@@ -144,6 +141,9 @@ converse.plugins.add('converse-rosterview', {
},
initJIDAutoComplete () {
if (!_converse.autocomplete_add_contact) {
return;
}
const el = this.el.querySelector('.suggestion-box__jid').parentElement;
this.jid_auto_complete = new _converse.AutoComplete(el, {
'data': (text, input) => `${input.slice(0, input.indexOf("@"))}@${text}`,
......@@ -153,6 +153,9 @@ converse.plugins.add('converse-rosterview', {
},
initXHRAutoComplete () {
if (!_converse.autocomplete_add_contact) {
return this.initXHRFetch();
}
const el = this.el.querySelector('.suggestion-box__name').parentElement;
this.name_auto_complete = new _converse.AutoComplete(el, {
'auto_evaluate': false,
......@@ -174,25 +177,66 @@ converse.plugins.add('converse-rosterview', {
xhr.open("GET", `${_converse.xhr_user_search_url}q=${input_el.value}`, true);
xhr.send()
} , 300));
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;
});
},
addContactFromForm (ev) {
ev.preventDefault();
const data = new FormData(ev.target),
jid = data.get('jid'),
name = data.get('name');
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) {
const el = this.el.querySelector('.suggestion-box__name .invalid-feedback');
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) {
if (!jid || _.compact(jid.split('@')).length < 2) {
// XXX: we used to have to do this manually, instead of via
// toHTML because Awesomplete messes things up and
// confuses Snabbdom
// We now use _converse.AutoComplete, can this be removed?
u.addClass('is-invalid', this.el.querySelector('input[name="jid"]'));
u.addClass('d-block', this.el.querySelector('.invalid-feedback'));
} else {
ev.target.reset();
_converse.roster.addAndSubscribe(jid, name);
this.model.clear();
this.modal.hide();
u.addClass('d-block', this.el.querySelector('.suggestion-box__jid .invalid-feedback'));
return false;
}
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),
jid = data.get('jid');
if (!jid && _converse.xhr_user_search_url && _.isString(_converse.xhr_user_search_url)) {
const input_el = this.el.querySelector('input[name="name"]');
this.xhr.open("GET", `${_converse.xhr_user_search_url}q=${input_el.value}`, true);
this.xhr.send()
return;
}
if (this.validateSubmission(jid)) {
this.afterSubmission(ev.target, jid, data.get('name'));
}
}
});
......
......@@ -453,7 +453,7 @@ converse.plugins.add('converse-roster', {
* (Function) callback - A function to call once the IQ is returned
* (Function) errback - A function to call if an error occurred
*/
name = _.isEmpty(name)? jid: name;
name = _.isEmpty(name) ? null : name;
const iq = $iq({'type': 'set'})
.c('query', {'xmlns': Strophe.NS.ROSTER})
.c('item', { jid, name });
......
......@@ -12,7 +12,9 @@
<label class="clearfix" for="jid">{{{o.label_xmpp_address}}}:</label>
<div class="suggestion-box suggestion-box__jid">
<ul class="suggestion-box__results suggestion-box__results--above" hidden=""></ul>
<input type="text" name="jid" required="required" value="{{{o.jid}}}"
<input type="text" name="jid"
{[ if (!o._converse.xhr_user_search_url) { ]} required="required" {[ } ]}
value="{{{o.jid}}}"
class="form-control suggestion-box__input"
placeholder="{{{o.contact_placeholder}}}"/>
<div class="invalid-feedback">{{{o.error_message}}}</div>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment