Re-add xhr_user_search_url and autocomplete when adding contacts

This commit is contained in:
JC Brand 2018-03-26 15:38:03 +02:00
parent 1e927294e0
commit 95406a149d
13 changed files with 340 additions and 248 deletions

View File

@ -149,7 +149,7 @@
"no-negated-condition": "off",
"no-negated-in-lhs": "error",
"no-nested-ternary": "off",
"no-new": "error",
"no-new": "off",
"no-new-func": "error",
"no-new-object": "error",
"no-new-require": "error",

View File

@ -7,7 +7,7 @@ addons:
chrome: stable
node_js:
- 6
install: make node_modules
install: make stamp-npm
before_script: make serve_bg
script: make check
sudo: false

View File

@ -2,29 +2,19 @@
## 4.0.0 (Unreleased)
## Removed configuration settings
## UI changes
Due to rewriting parts of the code, we regrettably had to remove certain
lesser-used configuration settings because the cost of adding them to the
new code was too high.
The UI is now based on Bootstrap4 and Flexbox is used extensively.
If you relied on any of these settings, you can reproduce their
functionality in your own 3rd party plugins, or you can [contact us](http://opkode.com/contact.html)
with regards to sponsoring development on reintroducing them.
## Configuration changes
* Removed the `xhr_custom_status` and `xhr_custom_status_url` configuration
settings. If you relied on these settings, you can instead listen for the
[statusMessageChanged](https://conversejs.org/docs/html/events.html#contactstatusmessagechanged)
event and make the XMLHttpRequest yourself.
* Removed the `xhr_user_search` and `xhr_user_search_url` configuration options.
## Updated UI
The UI is now rewritten with Bootstrap4 and Flexbox is used pretty much
everywhere. Unfortunately this means that in the overlayed view_mode, chat
boxes can no longer be resized horizontally (or diagonally). Perhaps a solution
for this can again be found, but time constraints meant that this feature had
to be removed.
* Removed `xhr_user_search` in favor of only accepting `xhr_user_search_url` as configuration option.
* The data returned from the `xhr_user_search_url` must now include the user's
`jid` instead of just an `id`.
### Bugfixes

View File

@ -8586,20 +8586,23 @@ body.reset {
#conversejs:not(.fullscreen) #minimized-chats .chat-head-message-count-hidden {
display: none; }
#converse-embedded-chat,
#conversejs {
/* Pointer */ }
#converse-embedded-chat [hidden],
#conversejs [hidden] {
display: none; }
#converse-embedded-chat .visually-hidden,
#conversejs .visually-hidden {
position: absolute;
clip: rect(0, 0, 0, 0); }
#converse-embedded-chat div.awesomplete,
#conversejs div.awesomplete {
display: inline-block;
position: relative; }
#converse-embedded-chat [hidden],
#conversejs [hidden] {
display: none; }
#converse-embedded-chat .visually-hidden,
#conversejs .visually-hidden {
position: absolute;
clip: rect(0, 0, 0, 0); }
#converse-embedded-chat .form-group .awesomplete,
#conversejs .form-group .awesomplete {
width: 100%; }
#converse-embedded-chat div.awesomplete,
#conversejs div.awesomplete {
display: inline-block;
position: relative; }
#converse-embedded-chat div.awesomplete mark,
#conversejs div.awesomplete mark {
background: #FFB9A7; }
#converse-embedded-chat div.awesomplete > input,
#conversejs div.awesomplete > input {
display: block; }
@ -8620,62 +8623,60 @@ body.reset {
border: 1px solid rgba(0, 0, 0, 0.3);
box-shadow: 0.05em 0.2em 0.6em rgba(0, 0, 0, 0.2);
text-shadow: none; }
#converse-embedded-chat div.awesomplete > ul:before,
#conversejs div.awesomplete > ul:before {
content: "";
position: absolute;
top: -.43em;
left: 1em;
width: 0;
height: 0;
background: white;
border: inherit;
border-right: 0;
border-bottom: 0;
-webkit-transform: rotate(45deg);
transform: rotate(45deg); }
#converse-embedded-chat div.awesomplete > ul > li,
#conversejs div.awesomplete > ul > li {
text-overflow: ellipsis;
overflow-x: hidden;
position: relative;
cursor: pointer;
padding: 1em; }
#converse-embedded-chat div.awesomplete > ul[hidden],
#converse-embedded-chat div.awesomplete > ul:empty,
#conversejs div.awesomplete > ul[hidden],
#conversejs div.awesomplete > ul:empty {
display: none; }
@supports (transform: scale(0)) {
#converse-embedded-chat div.awesomplete > ul,
#conversejs div.awesomplete > ul {
transition: 0.3s cubic-bezier(0.4, 0.2, 0.5, 1.4);
transform-origin: 1.43em -.43em; }
#converse-embedded-chat div.awesomplete > ul[hidden],
#converse-embedded-chat div.awesomplete > ul:empty,
#conversejs div.awesomplete > ul[hidden],
#conversejs div.awesomplete > ul:empty {
display: none; }
@supports (transform: scale(0)) {
#converse-embedded-chat div.awesomplete > ul,
#conversejs div.awesomplete > ul {
transition: 0.3s cubic-bezier(0.4, 0.2, 0.5, 1.4);
transform-origin: 1.43em -.43em; }
#converse-embedded-chat div.awesomplete > ul[hidden],
#converse-embedded-chat div.awesomplete > ul:empty,
#conversejs div.awesomplete > ul[hidden],
#conversejs div.awesomplete > ul:empty {
opacity: 0;
transform: scale(0);
display: block;
transition-timing-function: ease; } }
#converse-embedded-chat div.awesomplete > ul:before,
#conversejs div.awesomplete > ul:before {
content: "";
position: absolute;
top: -.43em;
left: 1em;
width: 0;
height: 0;
background: white;
border: inherit;
border-right: 0;
border-bottom: 0;
-webkit-transform: rotate(45deg);
transform: rotate(45deg); }
#converse-embedded-chat div.awesomplete > ul > li,
#conversejs div.awesomplete > ul > li {
text-overflow: ellipsis;
overflow-x: hidden;
position: relative;
cursor: pointer; }
#converse-embedded-chat div.awesomplete > ul > li:hover,
#conversejs div.awesomplete > ul > li:hover {
background: #E77051;
color: white; }
#converse-embedded-chat div.awesomplete > ul > li[aria-selected="true"],
#conversejs div.awesomplete > ul > li[aria-selected="true"] {
background: #3d6d8f;
color: white; }
#converse-embedded-chat div.awesomplete mark,
#conversejs div.awesomplete mark {
background: #FFB9A7; }
#converse-embedded-chat div.awesomplete li:hover mark,
#conversejs div.awesomplete li:hover mark {
background: #A53214;
color: white; }
#converse-embedded-chat div.awesomplete li[aria-selected="true"] mark,
#conversejs div.awesomplete li[aria-selected="true"] mark {
background: #3d6b00;
color: inherit; }
opacity: 0;
transform: scale(0);
display: block;
transition-timing-function: ease; } }
#converse-embedded-chat div.awesomplete > ul > li:hover,
#conversejs div.awesomplete > ul > li:hover {
background: #E77051;
color: white; }
#converse-embedded-chat div.awesomplete > ul > li[aria-selected="true"],
#conversejs div.awesomplete > ul > li[aria-selected="true"] {
background: #3d6d8f;
color: white; }
#converse-embedded-chat div.awesomplete li:hover mark,
#conversejs div.awesomplete li:hover mark {
background: #A53214;
color: white; }
#converse-embedded-chat div.awesomplete li[aria-selected="true"] mark,
#conversejs div.awesomplete li[aria-selected="true"] mark {
background: #3d6b00;
color: inherit; }
/*# sourceMappingURL=converse.css.map */

View File

@ -8698,20 +8698,23 @@ body {
border: 1.2em solid #E7A151;
border-top: 0.8em solid #E7A151; }
#converse-embedded-chat,
#conversejs {
/* Pointer */ }
#converse-embedded-chat [hidden],
#conversejs [hidden] {
display: none; }
#converse-embedded-chat .visually-hidden,
#conversejs .visually-hidden {
position: absolute;
clip: rect(0, 0, 0, 0); }
#converse-embedded-chat div.awesomplete,
#conversejs div.awesomplete {
display: inline-block;
position: relative; }
#converse-embedded-chat [hidden],
#conversejs [hidden] {
display: none; }
#converse-embedded-chat .visually-hidden,
#conversejs .visually-hidden {
position: absolute;
clip: rect(0, 0, 0, 0); }
#converse-embedded-chat .form-group .awesomplete,
#conversejs .form-group .awesomplete {
width: 100%; }
#converse-embedded-chat div.awesomplete,
#conversejs div.awesomplete {
display: inline-block;
position: relative; }
#converse-embedded-chat div.awesomplete mark,
#conversejs div.awesomplete mark {
background: #FFB9A7; }
#converse-embedded-chat div.awesomplete > input,
#conversejs div.awesomplete > input {
display: block; }
@ -8732,62 +8735,60 @@ body {
border: 1px solid rgba(0, 0, 0, 0.3);
box-shadow: 0.05em 0.2em 0.6em rgba(0, 0, 0, 0.2);
text-shadow: none; }
#converse-embedded-chat div.awesomplete > ul:before,
#conversejs div.awesomplete > ul:before {
content: "";
position: absolute;
top: -.43em;
left: 1em;
width: 0;
height: 0;
background: white;
border: inherit;
border-right: 0;
border-bottom: 0;
-webkit-transform: rotate(45deg);
transform: rotate(45deg); }
#converse-embedded-chat div.awesomplete > ul > li,
#conversejs div.awesomplete > ul > li {
text-overflow: ellipsis;
overflow-x: hidden;
position: relative;
cursor: pointer;
padding: 1em; }
#converse-embedded-chat div.awesomplete > ul[hidden],
#converse-embedded-chat div.awesomplete > ul:empty,
#conversejs div.awesomplete > ul[hidden],
#conversejs div.awesomplete > ul:empty {
display: none; }
@supports (transform: scale(0)) {
#converse-embedded-chat div.awesomplete > ul,
#conversejs div.awesomplete > ul {
transition: 0.3s cubic-bezier(0.4, 0.2, 0.5, 1.4);
transform-origin: 1.43em -.43em; }
#converse-embedded-chat div.awesomplete > ul[hidden],
#converse-embedded-chat div.awesomplete > ul:empty,
#conversejs div.awesomplete > ul[hidden],
#conversejs div.awesomplete > ul:empty {
display: none; }
@supports (transform: scale(0)) {
#converse-embedded-chat div.awesomplete > ul,
#conversejs div.awesomplete > ul {
transition: 0.3s cubic-bezier(0.4, 0.2, 0.5, 1.4);
transform-origin: 1.43em -.43em; }
#converse-embedded-chat div.awesomplete > ul[hidden],
#converse-embedded-chat div.awesomplete > ul:empty,
#conversejs div.awesomplete > ul[hidden],
#conversejs div.awesomplete > ul:empty {
opacity: 0;
transform: scale(0);
display: block;
transition-timing-function: ease; } }
#converse-embedded-chat div.awesomplete > ul:before,
#conversejs div.awesomplete > ul:before {
content: "";
position: absolute;
top: -.43em;
left: 1em;
width: 0;
height: 0;
background: white;
border: inherit;
border-right: 0;
border-bottom: 0;
-webkit-transform: rotate(45deg);
transform: rotate(45deg); }
#converse-embedded-chat div.awesomplete > ul > li,
#conversejs div.awesomplete > ul > li {
text-overflow: ellipsis;
overflow-x: hidden;
position: relative;
cursor: pointer; }
#converse-embedded-chat div.awesomplete > ul > li:hover,
#conversejs div.awesomplete > ul > li:hover {
background: #E77051;
color: white; }
#converse-embedded-chat div.awesomplete > ul > li[aria-selected="true"],
#conversejs div.awesomplete > ul > li[aria-selected="true"] {
background: #3d6d8f;
color: white; }
#converse-embedded-chat div.awesomplete mark,
#conversejs div.awesomplete mark {
background: #FFB9A7; }
#converse-embedded-chat div.awesomplete li:hover mark,
#conversejs div.awesomplete li:hover mark {
background: #A53214;
color: white; }
#converse-embedded-chat div.awesomplete li[aria-selected="true"] mark,
#conversejs div.awesomplete li[aria-selected="true"] mark {
background: #3d6b00;
color: inherit; }
opacity: 0;
transform: scale(0);
display: block;
transition-timing-function: ease; } }
#converse-embedded-chat div.awesomplete > ul > li:hover,
#conversejs div.awesomplete > ul > li:hover {
background: #E77051;
color: white; }
#converse-embedded-chat div.awesomplete > ul > li[aria-selected="true"],
#conversejs div.awesomplete > ul > li[aria-selected="true"] {
background: #3d6d8f;
color: white; }
#converse-embedded-chat div.awesomplete li:hover mark,
#conversejs div.awesomplete li:hover mark {
background: #A53214;
color: white; }
#converse-embedded-chat div.awesomplete li[aria-selected="true"] mark,
#conversejs div.awesomplete li[aria-selected="true"] mark {
background: #3d6b00;
color: inherit; }
/*# sourceMappingURL=inverse.css.map */

View File

@ -1535,3 +1535,38 @@ Example:
whitelisted_plugins: ['myplugin']
});
});
xhr_user_search_url
-------------------
.. note::
XHR stands for XMLHTTPRequest, and is meant here in the AJAX sense (Asynchronous JavaScript and XML).
* Default: ``null``
There are two ways to add users.
* The user inputs a valid JID (Jabber ID, aka XMPP address), and the user is added as a pending contact.
* The user inputs some text (for example part of a first name or last name),
an XHR (Ajax Request) will be made to a remote server, and a list of matches are returned.
The user can then choose one of the matches to add as a contact.
By providing an XHR search URL, you're enabling the second mechanism.
*What is expected from the remote server?*
A default JSON encoded list of objects must be returned. Each object
corresponds to a matched user and needs the keys ``jid`` and ``fullname``.
.. code-block:: javascript
[{"jid": "marty@mcfly.net", "fullname": "Marty McFly"}, {"jid": "doc@brown.com", "fullname": "Doc Brown"}]
.. note::
Make sure your server script sets the header `Content-Type: application/json`.
This is the URL to which an XHR GET request will be made to fetch user data from your remote server.
The query string will be included in the request with ``q`` as its key.
The data returned must be a JSON encoded list of user JIDs.

View File

@ -7,31 +7,62 @@
clip: rect(0, 0, 0, 0);
}
.form-group {
.awesomplete {
width: 100%;
}
}
div.awesomplete {
display: inline-block;
position: relative;
}
mark {
background: $lightest-red;
}
div.awesomplete > input {
display: block;
}
> input {
display: block;
}
div.awesomplete > ul {
position: absolute;
left: 0;
right: 0;
z-index: 1;
min-width: 100%;
box-sizing: border-box;
list-style: none;
padding: 0;
border-radius: .3em;
margin: .2em 0 0;
background: hsla(0,0%,100%,.9);
background: linear-gradient(to bottom right, white, hsla(0,0%,100%,.8));
border: 1px solid rgba(0,0,0,.3);
box-shadow: .05em .2em .6em rgba(0,0,0,.2);
text-shadow: none;
> ul {
&:before {
content: "";
position: absolute;
top: -.43em;
left: 1em;
width: 0; height: 0;
background: white;
border: inherit;
border-right: 0;
border-bottom: 0;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}
position: absolute;
left: 0;
right: 0;
z-index: 1;
min-width: 100%;
box-sizing: border-box;
list-style: none;
padding: 0;
border-radius: .3em;
margin: .2em 0 0;
background: hsla(0,0%,100%,.9);
background: linear-gradient(to bottom right, white, hsla(0,0%,100%,.8));
border: 1px solid rgba(0,0,0,.3);
box-shadow: .05em .2em .6em rgba(0,0,0,.2);
text-shadow: none;
> li {
text-overflow: ellipsis;
overflow-x: hidden;
position: relative;
cursor: pointer;
padding: 1em;
}
}
}
div.awesomplete > ul[hidden],
@ -54,28 +85,6 @@
}
}
/* Pointer */
div.awesomplete > ul:before {
content: "";
position: absolute;
top: -.43em;
left: 1em;
width: 0; height: 0;
background: white;
border: inherit;
border-right: 0;
border-bottom: 0;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}
div.awesomplete > ul > li {
text-overflow: ellipsis;
overflow-x: hidden;
position: relative;
cursor: pointer;
}
div.awesomplete > ul > li:hover {
background: $red;
color: $inverse-link-color;
@ -86,10 +95,6 @@
color: white;
}
div.awesomplete mark {
background: $lightest-red;
}
div.awesomplete li:hover mark {
background: $darkest-red;
color: $inverse-link-color;

View File

@ -1353,20 +1353,12 @@
$(view.el).find('.chat-area').remove();
test_utils.waitUntil(function () {
return $(view.el).find('input.invited-contact').length;
return $(view.el).find('input.invited-contact').length;
}, 300).then(function () {
var $input = $(view.el).find('input.invited-contact');
expect($input.attr('placeholder')).toBe('Invite');
$input.val("Felix");
var evt;
// check if Event() is a constructor function
// usage as per the spec, if true
if (typeof(Event) === 'function') {
evt = new Event('input');
} else { // the deprecated way for PhantomJS
evt = document.createEvent('CustomEvent');
evt.initCustomEvent('input', false, false, null);
}
var evt = new Event('input');
$input[0].dispatchEvent(evt);
var sent_stanza;

View File

@ -33,6 +33,37 @@
describe("The \"Contacts\" section", function () {
it("can be used to add contact and it checks for case-sensivity",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough();
test_utils.openControlBox();
// Adding two contacts one with Capital initials and one with small initials of same JID (Case sensitive check)
_converse.roster.create({
jid: mock.pend_names[0].replace(/ /g,'.').toLowerCase() + '@localhost',
subscription: 'none',
ask: 'subscribe',
fullname: mock.pend_names[0]
});
_converse.roster.create({
jid: mock.pend_names[0].replace(/ /g,'.') + '@localhost',
subscription: 'none',
ask: 'subscribe',
fullname: mock.pend_names[0]
});
test_utils.waitUntil(function () {
return $(_converse.rosterview.el).find('.roster-group li:visible').length;
}, 700).then(function () {
// Checking that only one entry is created because both JID is same (Case sensitive check)
expect($(_converse.rosterview.el).find('li:visible').length).toBe(1);
expect(_converse.rosterview.update).toHaveBeenCalled();
done();
});
}));
it("shows the number of unread mentions received",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
@ -157,6 +188,8 @@
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
test_utils.createContacts(_converse, 'all').openControlBox();
var panel = _converse.chatboxviews.get('controlbox').contactspanel;
var cbview = _converse.chatboxviews.get('controlbox');
cbview.el.querySelector('.add-contact').click()
@ -165,37 +198,55 @@
return u.isVisible(modal.el);
}, 1000).then(function () {
expect(!_.isNull(modal.el.querySelector('form.add-xmpp-contact'))).toBeTruthy();
var input_el = modal.el.querySelector('input[name="jid"]');
input_el.value = 'someone@';
var evt = new Event('input');
input_el.dispatchEvent(evt);
expect(modal.el.querySelector('.awesomplete li').textContent).toBe('someone@localhost');
done();
});
}));
it("can be used to add contact and it checks for case-sensivity",
it("integrates with xhr_user_search_url to search for contacts",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
null, ['rosterGroupsFetched'],
{ 'xhr_user_search': true,
'xhr_user_search_url': 'http://example.org/'
},
function (done, _converse) {
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough();
test_utils.openControlBox();
// Adding two contacts one with Capital initials and one with small initials of same JID (Case sensitive check)
_converse.roster.create({
jid: mock.pend_names[0].replace(/ /g,'.').toLowerCase() + '@localhost',
subscription: 'none',
ask: 'subscribe',
fullname: mock.pend_names[0]
var xhr = {
'open': _.noop,
'send': function () {
xhr.responseText = JSON.stringify([
{"jid": "marty@mcfly.net", "fullname": "Marty McFly"},
{"jid": "doc@brown.com", "fullname": "Doc Brown"}
]);
xhr.onload();
}
};
window.XMLHttpRequest = jasmine.createSpy('XMLHttpRequest');
XMLHttpRequest.and.callFake(function () {
return xhr;
});
_converse.roster.create({
jid: mock.pend_names[0].replace(/ /g,'.') + '@localhost',
subscription: 'none',
ask: 'subscribe',
fullname: mock.pend_names[0]
});
test_utils.waitUntil(function () {
return $(_converse.rosterview.el).find('.roster-group li:visible').length;
}, 700).then(function () {
// Checking that only one entry is created because both JID is same (Case sensitive check)
expect($(_converse.rosterview.el).find('li:visible').length).toBe(1);
expect(_converse.rosterview.update).toHaveBeenCalled();
var panel = _converse.chatboxviews.get('controlbox').contactspanel;
var cbview = _converse.chatboxviews.get('controlbox');
cbview.el.querySelector('.add-contact').click()
var modal = _converse.rosterview.add_contact_modal;
return test_utils.waitUntil(function () {
return u.isVisible(modal.el);
}, 1000).then(function () {
var input_el = modal.el.querySelector('input[name="jid"]');
input_el.value = 'marty@';
var evt = new Event('input');
input_el.dispatchEvent(evt);
return test_utils.waitUntil(function () {
return modal.el.querySelector('.awesomplete li');
});
}).then(function () {
expect(modal.el.querySelector('.awesomplete li').textContent).toBe('marty@mcfly.net');
done();
});
}));

View File

@ -16,6 +16,7 @@
"tpl!roster_filter",
"tpl!roster_item",
"tpl!search_contact",
"awesomplete",
"converse-chatboxes",
"converse-modal"
], factory);
@ -28,7 +29,8 @@
tpl_roster,
tpl_roster_filter,
tpl_roster_item,
tpl_search_contact
tpl_search_contact,
Awesomplete
) {
"use strict";
const { Backbone, Strophe, $iq, b64_sha1, sizzle, _ } = converse.env;
@ -78,9 +80,10 @@
{ __ } = _converse;
_converse.api.settings.update({
allow_chat_pending_contacts: true,
allow_contact_removal: true,
show_toolbar: true,
'allow_chat_pending_contacts': true,
'allow_contact_removal': true,
'show_toolbar': true,
'xhr_user_search_url': null
});
_converse.api.promises.add('rosterViewInitialized');
@ -147,6 +150,31 @@
}));
},
afterRender () {
const input_el = this.el.querySelector('input[name="jid"]');
if (_converse.xhr_user_search_url && _.isString(_converse.xhr_user_search_url)) {
const awesomplete = new Awesomplete(input_el, {'list': [], 'minChars': 2});
const xhr = new window.XMLHttpRequest();
// `open` must be called after `onload` for
// mock/testing purposes.
xhr.onload = function () {
awesomplete.list = JSON.parse(xhr.responseText).map((i) => i.jid);
awesomplete.evaluate();
};
xhr.open("GET", _converse.xhr_user_search_url, true);
input_el.addEventListener('input', _.debounce(() => xhr.send()), 100, {'leading': true});
} else {
const list = _.uniq(_converse.roster.map((item) => Strophe.getDomainFromJid(item.get('jid'))));
new Awesomplete(input_el, {
'list': list,
'data': function (text, input) {
return input.slice(0, input.indexOf("@")) + "@" + text;
},
'filter': Awesomplete.FILTER_STARTSWITH
});
}
},
addContactFromForm (ev) {
ev.preventDefault();
const data = new FormData(ev.target),

View File

@ -1,11 +0,0 @@
<form class="pure-form add-xmpp-contact">
{[ if (o.error_message) { ]}
<span class="pure-form-message error">{{{o.error_message}}}</span>
{[ } ]}
<input type="text"
name="identifier"
value="{{{o.value}}}"
class="username {[ if (o.error_message) { ]} error {[ } ]}"
placeholder="{{{o.label_contact_username}}}"/>
<button class="btn btn-primary" type="submit">{{{o.label_add}}}</button>
</form>

View File

@ -9,7 +9,7 @@
<form class="converse-form add-xmpp-contact">
<div class="modal-body">
<div class="form-group">
<label for="jid">{{{o.label_xmpp_address}}}:</label>
<label class="clearfix" for="jid">{{{o.label_xmpp_address}}}:</label>
<input type="text" name="jid" required="required" value="{{{o.jid}}}"
class="form-control {[ if (o.error_message) { ]} is-invalid {[ } ]}"
placeholder="{{{o.contact_placeholder}}}">

View File

@ -1,6 +1,6 @@
<form class="room-invite">
{[ if (o.error_message) { ]}
<span class="pure-form-message error">{{{o.error_message}}}</span>
<span class="error">{{{o.error_message}}}</span>
{[ } ]}
<input class="form-control invited-contact" placeholder="{{{o.label_invitation}}}" type="text"/>
</form>