Commit e344bf76 authored by JC Brand's avatar JC Brand

New modal for adding contacts.

Remove the xhr_user_search and xhr_user_search_url options
Lazily create modals
parent b3409fd0
...@@ -2,10 +2,21 @@ ...@@ -2,10 +2,21 @@
## 4.0.0 (Unreleased) ## 4.0.0 (Unreleased)
## Removed configuration settings
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.
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.
* Removed the `xhr_custom_status` and `xhr_custom_status_url` configuration * Removed the `xhr_custom_status` and `xhr_custom_status_url` configuration
settings. If you relied on these settings, you can instead listen for the settings. If you relied on these settings, you can instead listen for the
[statusMessageChanged](https://conversejs.org/docs/html/events.html#contactstatusmessagechanged) [statusMessageChanged](https://conversejs.org/docs/html/events.html#contactstatusmessagechanged)
event and make the XMLHttpRequest yourself. event and make the XMLHttpRequest yourself.
* Removed the `xhr_user_search` and `xhr_user_search_url` configuration options.
## 3.3.4 (Unreleased) ## 3.3.4 (Unreleased)
......
...@@ -1524,48 +1524,3 @@ An example from `the embedded room demo <https://conversejs.org/demo/embedded.ht ...@@ -1524,48 +1524,3 @@ An example from `the embedded room demo <https://conversejs.org/demo/embedded.ht
whitelisted_plugins: ['converse-muc-embedded'] whitelisted_plugins: ['converse-muc-embedded']
}); });
}); });
xhr_user_search
---------------
* Default: ``false``
.. note::
XHR stands for XMLHTTPRequest, and is meant here in the AJAX sense (Asynchronous JavaScript and XML).
There are two ways to add users.
* The user inputs a valid JID (Jabber ID), 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.
This setting enables the second mechanism, otherwise by default the first will be used.
*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 ``id`` and ``fullname``.
.. code-block:: javascript
[{"id": "foo", "fullname": "Foo McFoo"}, {"id": "bar", "fullname": "Bar McBar"}]
.. note::
Make sure your server script sets the header `Content-Type: application/json`.
xhr_user_search_url
-------------------
.. note::
XHR stands for XMLHTTPRequest, and is meant here in the AJAX sense (Asynchronous JavaScript and XML).
* Default: Empty string
Used only in conjunction with ``xhr_user_search``.
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.
...@@ -124,7 +124,7 @@ ...@@ -124,7 +124,7 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form class="pure-form add-xmpp-contact"> <form class="converse-form add-xmpp-contact">
<div class="form-group"> <div class="form-group">
<input type="text" name="identifier" class="form-control" placeholder="Contact username"> <input type="text" name="identifier" class="form-control" placeholder="Contact username">
</div> </div>
......
...@@ -9,13 +9,10 @@ ...@@ -9,13 +9,10 @@
(function (root, factory) { (function (root, factory) {
define(["converse-core", define(["converse-core",
"lodash.fp", "lodash.fp",
"tpl!add_contact_dropdown",
"tpl!add_contact_form",
"tpl!converse_brand_heading", "tpl!converse_brand_heading",
"tpl!controlbox", "tpl!controlbox",
"tpl!controlbox_toggle", "tpl!controlbox_toggle",
"tpl!login_panel", "tpl!login_panel",
"tpl!search_contact",
"converse-chatview", "converse-chatview",
"converse-rosterview", "converse-rosterview",
"converse-profile" "converse-profile"
...@@ -23,13 +20,10 @@ ...@@ -23,13 +20,10 @@
}(this, function ( }(this, function (
converse, converse,
fp, fp,
tpl_add_contact_dropdown,
tpl_add_contact_form,
tpl_brand_heading, tpl_brand_heading,
tpl_controlbox, tpl_controlbox,
tpl_controlbox_toggle, tpl_controlbox_toggle,
tpl_login_panel, tpl_login_panel
tpl_search_contact
) { ) {
"use strict"; "use strict";
...@@ -86,7 +80,7 @@ ...@@ -86,7 +80,7 @@
* *
* NB: These plugins need to have already been loaded via require.js. * NB: These plugins need to have already been loaded via require.js.
*/ */
dependencies: ["converse-chatboxes", "converse-rosterview", "converse-chatview"], dependencies: ["converse-modal", "converse-chatboxes", "converse-rosterview", "converse-chatview"],
overrides: { overrides: {
// Overrides mentioned here will be picked up by converse.js's // Overrides mentioned here will be picked up by converse.js's
...@@ -212,9 +206,7 @@ ...@@ -212,9 +206,7 @@
default_domain: undefined, default_domain: undefined,
locked_domain: undefined, locked_domain: undefined,
show_controlbox_by_default: false, show_controlbox_by_default: false,
sticky_controlbox: false, sticky_controlbox: false
xhr_user_search: false,
xhr_user_search_url: ''
}); });
_converse.api.promises.add('controlboxInitialized'); _converse.api.promises.add('controlboxInitialized');
...@@ -230,13 +222,6 @@ ...@@ -230,13 +222,6 @@
}) })
_converse.AddContactModal = Backbone.VDOMView.extend({
events: {
'submit form': 'addContact'
},
});
_converse.ControlBoxView = _converse.ChatBoxView.extend({ _converse.ControlBoxView = _converse.ChatBoxView.extend({
tagName: 'div', tagName: 'div',
className: 'chatbox', className: 'chatbox',
...@@ -528,12 +513,6 @@ ...@@ -528,12 +513,6 @@
_converse.ControlBoxPane = Backbone.NativeView.extend({ _converse.ControlBoxPane = Backbone.NativeView.extend({
tagName: 'div', tagName: 'div',
className: 'controlbox-pane', className: 'controlbox-pane',
events: {
'click a.toggle-xmpp-contact-form': 'toggleContactForm',
'submit form.add-xmpp-contact': 'addContactFromForm',
'submit form.search-xmpp-contact': 'searchContacts',
'click a.subscribe-to-user': 'addContactFromList'
},
initialize () { initialize () {
_converse.xmppstatusview = new _converse.XMPPStatusView({ _converse.xmppstatusview = new _converse.XMPPStatusView({
...@@ -543,103 +522,6 @@ ...@@ -543,103 +522,6 @@
'afterBegin', 'afterBegin',
_converse.xmppstatusview.render().el _converse.xmppstatusview.render().el
); );
},
generateAddContactHTML (settings={}) {
if (_converse.xhr_user_search) {
return tpl_search_contact({
label_contact_name: __('Contact name'),
label_search: __('Search')
});
} else {
return tpl_add_contact_form(_.assign({
error_message: null,
label_contact_username: __('e.g. user@example.org'),
label_add: __('Add'),
value: ''
}, settings));
}
},
toggleContactForm (ev) {
ev.preventDefault();
this.el.querySelector('.search-xmpp div').innerHTML = this.generateAddContactHTML();
var dropdown = this.el.querySelector('.contact-form-container');
u.slideToggleElement(dropdown).then(() => {
if (u.isVisible(dropdown)) {
dropdown.querySelector('input.username').focus();
}
});
},
searchContacts (ev) {
ev.preventDefault();
const search_query= ev.target.querySelector('input.username').value,
url = _converse.xhr_user_search_url+ "?q=" + search_query,
xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.setRequestHeader('Accept', "application/json, text/javascript");
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 400) {
const data = JSON.parse(xhr.responseText),
ul = _converse.root.querySelector('.search-xmpp ul');
u.removeElement(ul.querySelector('li.found-user'));
u.removeElement(ul.querySelector('li.chat-info'));
if (!data.length) {
const no_users_text = __('No users found');
ul.insertAdjacentHTML('beforeEnd', `<li class="chat-info">${no_users_text}</li>`);
}
else {
const title_subscribe = __('Click to add as a chat contact');
_.each(data, function (obj) {
const li = u.stringToElement('<li class="found-user"></li>'),
a = u.stringToElement(`<a class="subscribe-to-user" href="#" title="${title_subscribe}"></a>`),
jid = Strophe.getNodeFromJid(obj.id)+"@"+Strophe.getDomainFromJid(obj.id);
a.setAttribute('data-recipient', jid);
a.textContent = obj.fullname;
li.appendChild(a);
u.appendChild(li)
});
}
} else {
xhr.onerror();
}
};
xhr.onerror = function () {
_converse.log('Could not fetch contacts via XHR', Strophe.LogLevel.ERROR);
};
xhr.send();
},
addContactFromForm (ev) {
ev.preventDefault();
const input = ev.target.querySelector('input');
const jid = input.value;
if (!jid || _.compact(jid.split('@')).length < 2) {
this.el.querySelector('.search-xmpp div').innerHTML =
this.generateAddContactHTML({
error_message: __('Please enter a valid XMPP address'),
label_contact_username: __('e.g. user@example.org'),
label_add: __('Add'),
value: jid
});
return;
}
_converse.roster.addAndSubscribe(jid);
u.slideIn(this.el.querySelector('.contact-form-container'));
},
addContactFromList (ev) {
ev.preventDefault();
const jid = ev.target.getAttribute('data-recipient'),
name = ev.target.textContent;
_converse.roster.addAndSubscribe(jid, name);
const parent = ev.target.parentNode;
parent.parentNode.removeChild(parent);
u.slideIn(this.el.querySelector('.contact-form-container'));
} }
}); });
......
...@@ -38,9 +38,9 @@ ...@@ -38,9 +38,9 @@
}, },
show (ev) { show (ev) {
ev.preventDefault();
this.trigger_el = ev.target; this.trigger_el = ev.target;
this.trigger_el.classList.add('selected'); this.trigger_el.classList.add('selected');
this.render();
this.modal.show(); this.modal.show();
} }
}); });
......
...@@ -2184,11 +2184,6 @@ ...@@ -2184,11 +2184,6 @@
'click a.room-info': 'toggleRoomInfo' 'click a.room-info': 'toggleRoomInfo'
}, },
initialize (cfg) {
this.add_room_modal = new _converse.AddChatRoomModal({'model': this.model});
this.list_rooms_modal = new _converse.ListChatRoomsModal({'model': this.model});
},
render () { render () {
this.el.innerHTML = tpl_room_panel({ this.el.innerHTML = tpl_room_panel({
'heading_chatrooms': __('Chatrooms'), 'heading_chatrooms': __('Chatrooms'),
...@@ -2204,12 +2199,16 @@ ...@@ -2204,12 +2199,16 @@
}, },
showAddRoomModal (ev) { showAddRoomModal (ev) {
ev.preventDefault(); if (_.isUndefined(this.add_room_modal)) {
this.add_room_modal = new _converse.AddChatRoomModal({'model': this.model});
}
this.add_room_modal.show(ev); this.add_room_modal.show(ev);
}, },
showListRoomsModal(ev) { showListRoomsModal(ev) {
ev.preventDefault(); if (_.isUndefined(this.list_rooms_modal)) {
this.list_rooms_modal = new _converse.ListChatRoomsModal({'model': this.model});
}
this.list_rooms_modal.show(ev); this.list_rooms_modal.show(ev);
} }
}); });
......
...@@ -12,7 +12,8 @@ ...@@ -12,7 +12,8 @@
"tpl!chat_status_modal", "tpl!chat_status_modal",
"tpl!profile_view", "tpl!profile_view",
"tpl!status_option", "tpl!status_option",
"converse-vcard" "converse-vcard",
"converse-modal"
], factory); ], factory);
}(this, function ( }(this, function (
converse, converse,
...@@ -29,6 +30,8 @@ ...@@ -29,6 +30,8 @@
converse.plugins.add('converse-profile', { converse.plugins.add('converse-profile', {
dependencies: ["converse-modal"],
initialize () { initialize () {
/* The initialize function gets called as soon as the plugin is /* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery. * loaded by converse.js's plugin machinery.
...@@ -37,20 +40,12 @@ ...@@ -37,20 +40,12 @@
{ __ } = _converse; { __ } = _converse;
_converse.ChatStatusModal = Backbone.VDOMView.extend({ _converse.ChatStatusModal = _converse.BootstrapModal.extend({
events: { events: {
"submit form#set-xmpp-status": "onFormSubmitted", "submit form#set-xmpp-status": "onFormSubmitted",
"click .clear-input": "clearStatusMessage" "click .clear-input": "clearStatusMessage"
}, },
initialize () {
this.render().insertIntoDOM();
this.modal = new bootstrap.Modal(this.el, {
backdrop: 'static',
keyboard: true
});
},
toHTML () { toHTML () {
return tpl_chat_status_modal(_.extend(this.model.toJSON(), { return tpl_chat_status_modal(_.extend(this.model.toJSON(), {
'label_away': __('Away'), 'label_away': __('Away'),
...@@ -67,16 +62,6 @@ ...@@ -67,16 +62,6 @@
})); }));
}, },
insertIntoDOM () {
const container_el = _converse.chatboxviews.el.querySelector('#converse-modals');
container_el.insertAdjacentElement('beforeEnd', this.el);
},
show () {
this.render();
this.modal.show();
},
clearStatusMessage (ev) { clearStatusMessage (ev) {
if (ev && ev.preventDefault) { if (ev && ev.preventDefault) {
ev.preventDefault(); ev.preventDefault();
...@@ -107,7 +92,6 @@ ...@@ -107,7 +92,6 @@
initialize () { initialize () {
this.model.on("change", this.render, this); this.model.on("change", this.render, this);
this.status_modal = new _converse.ChatStatusModal({model: this.model});
}, },
toHTML () { toHTML () {
...@@ -125,8 +109,10 @@ ...@@ -125,8 +109,10 @@
}, },
showStatusChangeModal (ev) { showStatusChangeModal (ev) {
ev.preventDefault(); if (_.isUndefined(this.status_modal)) {
this.status_modal.show(); this.status_modal = new _converse.ChatStatusModal({model: this.model});
}
this.status_modal.show(ev);
}, },
logOut (ev) { logOut (ev) {
......
...@@ -8,22 +8,28 @@ ...@@ -8,22 +8,28 @@
(function (root, factory) { (function (root, factory) {
define(["converse-core", define(["converse-core",
"tpl!add_contact_modal",
"tpl!group_header", "tpl!group_header",
"tpl!pending_contact", "tpl!pending_contact",
"tpl!requesting_contact", "tpl!requesting_contact",
"tpl!roster", "tpl!roster",
"tpl!roster_filter", "tpl!roster_filter",
"tpl!roster_item", "tpl!roster_item",
"converse-chatboxes" "tpl!search_contact",
"converse-chatboxes",
"converse-modal"
], factory); ], factory);
}(this, function ( }(this, function (
converse, converse,
tpl_add_contact_modal,
tpl_group_header, tpl_group_header,
tpl_pending_contact, tpl_pending_contact,
tpl_requesting_contact, tpl_requesting_contact,
tpl_roster, tpl_roster,
tpl_roster_filter, tpl_roster_filter,
tpl_roster_item) { tpl_roster_item,
tpl_search_contact
) {
"use strict"; "use strict";
const { Backbone, Strophe, $iq, b64_sha1, sizzle, _ } = converse.env; const { Backbone, Strophe, $iq, b64_sha1, sizzle, _ } = converse.env;
const u = converse.env.utils; const u = converse.env.utils;
...@@ -31,6 +37,8 @@ ...@@ -31,6 +37,8 @@
converse.plugins.add('converse-rosterview', { converse.plugins.add('converse-rosterview', {
dependencies: ["converse-modal"],
overrides: { overrides: {
// Overrides mentioned here will be picked up by converse.js's // Overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the // plugin architecture they will replace existing methods on the
...@@ -118,6 +126,47 @@ ...@@ -118,6 +126,47 @@
}; };
_converse.AddContactModal = _converse.BootstrapModal.extend({
events: {
'click a.subscribe-to-user': 'addContactFromList',
'submit form': 'addContactFromForm',
'submit form.search-xmpp-contact': 'searchContacts'
},
initialize () {
_converse.BootstrapModal.prototype.initialize.apply(this, arguments);
this.model.on('change', this.render, this);
},
toHTML () {
return tpl_add_contact_modal(_.extend(this.model.toJSON(), {
'heading_new_contact': __('Add a Contact'),
'label_xmpp_address': __('XMPP Address'),
'label_nickname': __('Optional nickname'),
'contact_placeholder': __('name@example.org'),
'label_add': __('Add'),
}));
},
addContactFromForm (ev) {
ev.preventDefault();
const data = new FormData(ev.target),
jid = data.get('jid');
if (!jid || _.compact(jid.split('@')).length < 2) {
this.model.set({
'error_message': __('Please enter a valid XMPP address'),
'jid': jid
})
} else {
_converse.roster.addAndSubscribe(jid);
this.model.clear();
this.modal.hide();
}
}
});
_converse.RosterFilter = Backbone.Model.extend({ _converse.RosterFilter = Backbone.Model.extend({
initialize () { initialize () {
this.set({ this.set({
...@@ -620,6 +669,11 @@ ...@@ -620,6 +669,11 @@
sortEvent: null, // Groups are immutable, so they don't get re-sorted sortEvent: null, // Groups are immutable, so they don't get re-sorted
subviewIndex: 'name', subviewIndex: 'name',
events: {
'click a.chatbox-btn.add-contact': 'showAddContactModal',
},
initialize () { initialize () {
Backbone.OrderedListView.prototype.initialize.apply(this, arguments); Backbone.OrderedListView.prototype.initialize.apply(this, arguments);
...@@ -666,6 +720,13 @@ ...@@ -666,6 +720,13 @@
return this; return this;
}, },
showAddContactModal (ev) {
if (_.isUndefined(this.add_contact_modal)) {
this.add_contact_modal = new _converse.AddContactModal({'model': new Backbone.Model()});
}
this.add_contact_modal.show(ev);
},
createRosterFilter () { createRosterFilter () {
// Create a model on which we can store filter properties // Create a model on which we can store filter properties
const model = new _converse.RosterFilter(); const model = new _converse.RosterFilter();
......
<div class="modal fade" id="chatroomsModal" tabindex="-1" role="dialog" aria-labelledby="chatroomsModalLabel" aria-hidden="true"> <div class="modal fade" id="add-chatroom-modal" tabindex="-1" role="dialog" aria-labelledby="add-chatroom-modal-label" aria-hidden="true">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" <h5 class="modal-title"
id="chatroomsModalLabel">{{{o.heading_new_chatroom}}}</h5> id="add-chatroom-modal-label">{{{o.heading_new_chatroom}}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
...@@ -11,11 +11,11 @@ ...@@ -11,11 +11,11 @@
<div class="modal-body"> <div class="modal-body">
<form class="converse-form add-chatroom"> <form class="converse-form add-chatroom">
<div class="form-group"> <div class="form-group">
<label for="server">{{{o.label_room_address}}}:</label> <label for="chatroom">{{{o.label_room_address}}}:</label>
<input type="text" value="{{{o.muc_domain}}}" required="required" name="chatroom" class="form-control" placeholder="{{{o.chatroom_placeholder}}}"> <input type="text" required="required" name="chatroom" class="form-control" placeholder="{{{o.chatroom_placeholder}}}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="chatroom">{{{o.label_nickname}}}:</label> <label for="nickname">{{{o.label_nickname}}}:</label>
<input type="text" name="nickname" value="{{{o.nick}}}" class="form-control"> <input type="text" name="nickname" value="{{{o.nick}}}" class="form-control">
</div> </div>
<input type="submit" class="btn btn-primary" name="join" value="{{{o.label_join}}}"> <input type="submit" class="btn btn-primary" name="join" value="{{{o.label_join}}}">
......
<dl class="add-converse-contact dropdown">
<dt id="xmpp-contact-search" class="fancy-dropdown">
<a class="toggle-xmpp-contact-form icon-plus" href="#" title="{{{o.label_click_to_chat}}}"> {{{o.label_add_contact}}}</a>
</dt>
<dd class="search-xmpp">
<div class="contact-form-container collapsed"></div>
<ul></ul>
</dd>
</dl>
<!-- Add contact Modal -->
<div class="modal fade" id="add-contact-modal" tabindex="-1" role="dialog" aria-labelledby="addContactModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addContactModalLabel">{{{o.heading_new_contact}}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
</div>
<form class="converse-form add-xmpp-contact">
<div class="modal-body">
<div class="form-group">
<label 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}}}">
{[ if (o.error_message) { ]}
<div class="invalid-feedback">{{{o.error_message}}}</div>
{[ } ]}
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">{{{o.label_add}}}</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="list-chatrooms-modal" tabindex="-1" role="dialog" aria-labelledby="chatroomsModalLabel" aria-hidden="true"> <div class="modal fade" id="list-chatrooms-modal" tabindex="-1" role="dialog" aria-labelledby="list-chatrooms-modal-label" aria-hidden="true">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" <h5 class="modal-title"
id="chatroomsModalLabel">{{{o.heading_list_chatrooms}}}</h5> id="list-chatrooms-modal-label">{{{o.heading_list_chatrooms}}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form class="converse-form add-chatroom"> <form class="converse-form list-chatrooms">
<div class="form-group"> <div class="form-group">
<label for="chatroom">{{{o.label_server_address}}}:</label> <label for="chatroom">{{{o.label_server_address}}}:</label>
<input type="text" value="{{{o.muc_domain}}}" required="required" name="server" class="form-control" placeholder="{{{o.server_placeholder}}}"> <input type="text" value="{{{o.muc_domain}}}" required="required" name="server" class="form-control" placeholder="{{{o.server_placeholder}}}">
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<div class="d-flex"> <div class="d-flex">
<span class="w-100">{{{o.heading_chatrooms}}}</span> <span class="w-100">{{{o.heading_chatrooms}}}</span>
<a class="chatbox-btn fa fa-list-ul" title="{{{o.title_list_rooms}}}" data-toggle="modal" data-target="#list-chatrooms-modal"></a> <a class="chatbox-btn fa fa-list-ul" title="{{{o.title_list_rooms}}}" data-toggle="modal" data-target="#list-chatrooms-modal"></a>
<a class="chatbox-btn fa fa-users" title="{{{o.title_new_room}}}" data-toggle="modal" data-target="#chatroomsModal"></a> <a class="chatbox-btn fa fa-users" title="{{{o.title_new_room}}}" data-toggle="modal" data-target="#add-chatrooms-modal"></a>
</div> </div>
<div class="list-container open-rooms-list rooms-list-container"></div> <div class="list-container open-rooms-list rooms-list-container"></div>
<div class="list-container bookmarks-list rooms-list-container"></div> <div class="list-container bookmarks-list rooms-list-container"></div>
......
<div class="d-flex"> <div class="d-flex">
<span class="w-100">{{{o.heading_contacts}}}</span> <span class="w-100">{{{o.heading_contacts}}}</span>
<a class="chatbox-btn fa fa-user-plus" title="{{{o.title_add_contact}}}" <a class="chatbox-btn add-contact fa fa-user-plus" title="{{{o.title_add_contact}}}"
data-toggle="modal" data-target="#addContactModal"></a> data-toggle="modal" data-target="#add-contact-modal"></a>
</div> </div>
<form class="roster-filter-form"></form> <form class="roster-filter-form"></form>
......
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