소스 검색

Replace modal templates with lit-html components

JC Brand 5 년 전
부모
커밋
ad93407907
51개의 변경된 파일1024개의 추가작업 그리고 901개의 파일을 삭제
  1. 1 1
      .eslintrc.json
  2. 11 5
      package-lock.json
  3. 1 0
      package.json
  4. 1 1
      sass/_modal.scss
  5. 25 31
      spec/controlbox.js
  6. 1 1
      spec/messages.js
  7. 4 2
      spec/muc_messages.js
  8. 1 3
      spec/user-details-modal.js
  9. 6 5
      src/converse-chatboxviews.js
  10. 4 6
      src/converse-chatview.js
  11. 3 6
      src/converse-message-view.js
  12. 1 1
      src/converse-minimize.js
  13. 35 29
      src/converse-modal.js
  14. 8 12
      src/converse-muc-views.js
  15. 18 52
      src/converse-profile.js
  16. 4 11
      src/converse-rosterview.js
  17. 6 11
      src/headless/converse-core.js
  18. 6 1
      src/headless/i18n.js
  19. 1 1
      src/headless/package.json
  20. 4 4
      src/headless/polyfill.js
  21. 0 36
      src/templates/add_chatroom_modal.html
  22. 42 0
      src/templates/add_chatroom_modal.js
  23. 28 17
      src/templates/add_contact_modal.js
  24. 0 1
      src/templates/alert.html
  25. 3 0
      src/templates/alert.js
  26. 0 16
      src/templates/alert_modal.html
  27. 18 0
      src/templates/alert_modal.js
  28. 5 0
      src/templates/avatar.js
  29. 9 0
      src/templates/buttons.js
  30. 25 23
      src/templates/chat_status_modal.js
  31. 0 70
      src/templates/chatroom_details_modal.html
  32. 88 0
      src/templates/chatroom_details_modal.js
  33. 0 19
      src/templates/chatroom_registration_modal.html
  34. 0 21
      src/templates/client_info_modal.html
  35. 42 0
      src/templates/client_info_modal.js
  36. 0 25
      src/templates/list_chatrooms_modal.html
  37. 36 0
      src/templates/list_chatrooms_modal.js
  38. 0 20
      src/templates/message_versions_modal.html
  39. 27 0
      src/templates/message_versions_modal.js
  40. 0 219
      src/templates/moderator_tools_modal.html
  41. 229 0
      src/templates/moderator_tools_modal.js
  42. 32 0
      src/templates/profile.js
  43. 0 125
      src/templates/profile_modal.html
  44. 165 0
      src/templates/profile_modal.js
  45. 0 25
      src/templates/profile_view.html
  46. 0 30
      src/templates/prompt.html
  47. 37 0
      src/templates/prompt.js
  48. 3 0
      src/templates/spinner.js
  49. 0 71
      src/templates/user_details_modal.html
  50. 93 0
      src/templates/user_details_modal.js
  51. 1 0
      webpack.html

+ 1 - 1
.eslintrc.json

@@ -259,7 +259,7 @@
         "spaced-comment": "off",
         "strict": "off",
         "symbol-description": "error",
-        "template-curly-spacing": "error",
+        "template-curly-spacing": "off",
         "unicode-bom": [
             "error",
             "never"

+ 11 - 5
package-lock.json

@@ -2217,9 +2217,9 @@
 			}
 		},
 		"@octokit/types": {
-			"version": "2.1.0",
-			"resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.1.0.tgz",
-			"integrity": "sha512-n1GUYFgKm5glcy0E+U5jnqAFY2p04rnK4A0YhuM70C7Vm9Vyx+xYwd/WOTEr8nUJcbPSR/XL+/26+rirY6jJQA==",
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.1.1.tgz",
+			"integrity": "sha512-89LOYH+d/vsbDX785NOfLxTW88GjNd0lWRz1DVPVsZgg9Yett5O+3MOvwo7iHgvUwbFz0mf/yPIjBkUbs4kxoQ==",
 			"dev": true,
 			"requires": {
 				"@types/node": ">= 8"
@@ -8841,6 +8841,12 @@
 				"uc.micro": "^1.0.1"
 			}
 		},
+		"lit-html": {
+			"version": "1.1.2",
+			"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.1.2.tgz",
+			"integrity": "sha512-FFlUMKHKi+qG1x1iHNZ1hrtc/zHmfYTyrSvs3/wBTvaNtpZjOZGWzU7efGYVpgp6KvWeKF6ql9/KsCq6Z/mEDA==",
+			"dev": true
+		},
 		"load-json-file": {
 			"version": "5.3.0",
 			"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz",
@@ -16088,8 +16094,8 @@
 			"dev": true
 		},
 		"skeletor.js": {
-			"version": "github:skeletorjs/skeletor#abc4e9d25d30159e9cffc14bf5f7ffe17b3665eb",
-			"from": "github:skeletorjs/skeletor#abc4e9d25d30159e9cffc14bf5f7ffe17b3665eb",
+			"version": "github:skeletorjs/skeletor#29a6d8f707076e865133b8f36f07c76ba4b4b582",
+			"from": "github:skeletorjs/skeletor#29a6d8f707076e865133b8f36f07c76ba4b4b582",
 			"requires": {
 				"lodash": "^4.17.14"
 			}

+ 1 - 0
package.json

@@ -88,6 +88,7 @@
     "jasmine-core": "2.99.1",
     "jsdoc": "^3.6.2",
     "lerna": "^3.20.2",
+    "lit-html": "^1.1.2",
     "lodash-template-webpack-loader": "jcbrand/lodash-template-webpack-loader",
     "mini-css-extract-plugin": "^0.7.0",
     "minimist": "^1.2.0",

+ 1 - 1
sass/_modal.scss

@@ -41,7 +41,7 @@
         .set-xmpp-status {
             margin: 1em;
             .custom-control-label {
-                margin-top: 0.25em;
+                padding-top: 0.25em;
             }
         }
 

+ 25 - 31
spec/controlbox.js

@@ -199,27 +199,25 @@
             cbview.el.querySelector('.add-contact').click()
             const modal = _converse.rosterview.add_contact_modal;
             await u.waitUntil(() => u.isVisible(modal.el), 1000);
-            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);
-            });
-
-            expect(!_.isNull(modal.el.querySelector('form.add-xmpp-contact'))).toBeTruthy();
+            expect(modal.el.querySelector('form.add-xmpp-contact')).not.toBe(null);
+
             const input_jid = modal.el.querySelector('input[name="jid"]');
             const input_name = modal.el.querySelector('input[name="name"]');
             input_jid.value = 'someone@';
+
             const evt = new Event('input');
             input_jid.dispatchEvent(evt);
             expect(modal.el.querySelector('.suggestion-box li').textContent).toBe('someone@montague.lit');
             input_jid.value = 'someone@montague.lit';
             input_name.value = 'Someone';
             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="someone@montague.lit" name="Someone"/></query>`+
-            `</iq>`);
+
+            const sent_IQs = _converse.connection.IQ_stanzas;
+            const sent_stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.ROSTER}"]`)).pop());
+            expect(Strophe.serialize(sent_stanza)).toEqual(
+                `<iq id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">`+
+                    `<query xmlns="jabber:iq:roster"><item jid="someone@montague.lit" name="Someone"/></query>`+
+                `</iq>`);
             done();
         }));
 
@@ -228,6 +226,7 @@
                 ['rosterGroupsFetched'], {'autocomplete_add_contact': false},
                 async function (done, _converse) {
 
+            await test_utils.waitForRoster(_converse, 'all', 0);
             test_utils.openControlBox(_converse);
             const cbview = _converse.chatboxviews.get('controlbox');
             cbview.el.querySelector('.add-contact').click()
@@ -236,14 +235,14 @@
             expect(modal.name_auto_complete).toBe(undefined);
 
             await u.waitUntil(() => u.isVisible(modal.el), 1000);
-            expect(!_.isNull(modal.el.querySelector('form.add-xmpp-contact'))).toBeTruthy();
+            expect(modal.el.querySelector('form.add-xmpp-contact')).not.toBe(null);
             const input_jid = modal.el.querySelector('input[name="jid"]');
             input_jid.value = 'someone@montague.lit';
             modal.el.querySelector('button[type="submit"]').click();
 
             const IQ_stanzas = _converse.connection.IQ_stanzas;
             const sent_stanza = await u.waitUntil(
-                () => IQ_stanzas.filter(s => sizzle(`query[xmlns="${Strophe.NS.ROSTER}"]`, s).length).pop()
+                () => IQ_stanzas.filter(s => sizzle(`iq[type="set"] query[xmlns="${Strophe.NS.ROSTER}"]`, s).length).pop()
             );
             expect(Strophe.serialize(sent_stanza)).toEqual(
                 `<iq id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">`+
@@ -260,6 +259,8 @@
                 { 'xhr_user_search_url': 'http://example.org/?' },
                 async function (done, _converse) {
 
+            await test_utils.waitForRoster(_converse, 'all', 0);
+
             const xhr = {
                 'open': function open () {},
                 'send': function () {
@@ -287,12 +288,6 @@
             input_el.value = 'marty';
             input_el.dispatchEvent(new Event('input'));
             await u.waitUntil(() => modal.el.querySelector('.suggestion-box li'), 1000);
-            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);
-            });
             expect(modal.el.querySelectorAll('.suggestion-box li').length).toBe(1);
             const suggestion = modal.el.querySelector('.suggestion-box li');
             expect(suggestion.textContent).toBe('Marty McFly');
@@ -303,8 +298,11 @@
             expect(input_el.value).toBe('Marty McFly');
             expect(modal.el.querySelector('input[name="jid"]').value).toBe('marty@mcfly.net');
             modal.el.querySelector('button[type="submit"]').click();
-            expect(sent_stanza.toLocaleString()).toEqual(
-            `<iq id="${IQ_id}" type="set" xmlns="jabber:client">`+
+
+            const sent_IQs = _converse.connection.IQ_stanzas;
+            const sent_stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.ROSTER}"]`)).pop());
+            expect(Strophe.serialize(sent_stanza)).toEqual(
+            `<iq id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">`+
                 `<query xmlns="jabber:iq:roster"><item jid="marty@mcfly.net" name="Marty McFly"/></query>`+
             `</iq>`);
             window.XMLHttpRequest = XMLHttpRequestBackup;
@@ -355,13 +353,6 @@
             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();
@@ -382,8 +373,11 @@
 
             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">`+
+
+            const sent_IQs = _converse.connection.IQ_stanzas;
+            const sent_stanza = await u.waitUntil(() => sent_IQs.filter(iq => iq.querySelector(`iq[type="set"] query[xmlns="${Strophe.NS.ROSTER}"]`)).pop());
+            expect(Strophe.serialize(sent_stanza)).toEqual(
+            `<iq id="${sent_stanza.getAttribute('id')}" type="set" xmlns="jabber:client">`+
                 `<query xmlns="jabber:iq:roster"><item jid="marty@mcfly.net" name="Marty McFly"/></query>`+
             `</iq>`);
             window.XMLHttpRequest = XMLHttpRequestBackup;

+ 1 - 1
spec/messages.js

@@ -1506,7 +1506,7 @@
                 const older_msgs = modal.el.querySelectorAll('.older-msg');
                 expect(older_msgs.length).toBe(2);
                 expect(older_msgs[0].childNodes[0].nodeName).toBe('TIME');
-                expect(older_msgs[0].childNodes[1].textContent).toBe(': But soft, what light through yonder airlock breaks?');
+                expect(older_msgs[0].childNodes[2].textContent).toBe('But soft, what light through yonder airlock breaks?');
                 expect(view.model.messages.models.length).toBe(1);
                 done();
             }));

+ 4 - 2
spec/muc_messages.js

@@ -430,6 +430,8 @@
                     'type': 'groupchat',
                     'id': msg_id,
                 }).c('body').t('But soft, what light through yonder airlock breaks?').tree());
+
+            await u.waitUntil(() => view.el.querySelectorAll('.chat-msg').length);
             expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
             expect(view.el.querySelector('.chat-msg__text').textContent)
                 .toBe('But soft, what light through yonder airlock breaks?');
@@ -463,10 +465,10 @@
             await u.waitUntil(() => u.isVisible(modal.el), 1000);
             const older_msgs = modal.el.querySelectorAll('.older-msg');
             expect(older_msgs.length).toBe(2);
-            expect(older_msgs[0].childNodes[1].textContent).toBe(': But soft, what light through yonder airlock breaks?');
+            expect(older_msgs[0].childNodes[2].textContent).toBe('But soft, what light through yonder airlock breaks?');
             expect(older_msgs[0].childNodes[0].nodeName).toBe('TIME');
             expect(older_msgs[1].childNodes[0].nodeName).toBe('TIME');
-            expect(older_msgs[1].childNodes[1].textContent).toBe(': But soft, what light through yonder chimney breaks?');
+            expect(older_msgs[1].childNodes[2].textContent).toBe('But soft, what light through yonder chimney breaks?');
             done();
         }));
 

+ 1 - 3
spec/user-details-modal.js

@@ -28,9 +28,7 @@
             const modal = view.user_details_modal;
             await u.waitUntil(() => u.isVisible(modal.el), 1000);
             spyOn(window, 'confirm').and.returnValue(true);
-            spyOn(view.model.contact, 'removeFromRoster').and.callFake(function (callback) {
-                callback();
-            });
+            spyOn(view.model.contact, 'removeFromRoster').and.callFake(callback => callback());
             let remove_contact_button = modal.el.querySelector('button.remove-contact');
             expect(u.isVisible(remove_contact_button)).toBeTruthy();
             remove_contact_button.click();

+ 6 - 5
src/converse-chatboxviews.js

@@ -8,16 +8,17 @@
  * @module converse-chatboxviews
  */
 import "@converse/headless/converse-chatboxes";
-import "backbone.nativeview";
+import { HTMLView } from 'skeletor.js/src/htmlview.js';
 import { Overview } from "skeletor.js/src/overview";
 import { View } from "skeletor.js/src/view";
+import { result } from "lodash";
 import converse from "@converse/headless/converse-core";
 import tpl_avatar from "templates/avatar.svg";
 import tpl_background_logo from "templates/background_logo.html";
 import tpl_chatboxes from "templates/chatboxes.html";
 
-const { Backbone, _, utils } = converse.env;
-const u = utils;
+const u = converse.env.utils;
+
 
 const AvatarMixin = {
 
@@ -65,7 +66,7 @@ converse.plugins.add('converse-chatboxviews', {
         });
 
         _converse.ViewWithAvatar = View.extend(AvatarMixin);
-        _converse.VDOMViewWithAvatar = Backbone.VDOMView.extend(AvatarMixin);
+        _converse.HTMLViewWithAvatar = HTMLView.extend(AvatarMixin);
 
 
         _converse.ChatBoxViews = Overview.extend({
@@ -91,7 +92,7 @@ converse.plugins.add('converse-chatboxviews', {
                     el.innerHTML = '';
                     this.setElement(el, false);
                 } else {
-                    this.setElement(_.result(this, 'el'), false);
+                    this.setElement(result(this, 'el'), false);
                 }
             },
 

+ 4 - 6
src/converse-chatview.js

@@ -12,7 +12,6 @@ import "converse-message-view";
 import "converse-modal";
 import { debounce, get, isString } from "lodash";
 import { Overview } from "skeletor.js/src/overview";
-import { View } from "skeletor.js/src/view";
 import converse from "@converse/headless/converse-core";
 import log from "@converse/headless/log";
 import tpl_chatbox from "templates/chatbox.html";
@@ -27,7 +26,7 @@ import tpl_spoiler_button from "templates/spoiler_button.html";
 import tpl_status_message from "templates/status_message.html";
 import tpl_toolbar from "templates/toolbar.html";
 import tpl_toolbar_fileupload from "templates/toolbar_fileupload.html";
-import tpl_user_details_modal from "templates/user_details_modal.html";
+import tpl_user_details_modal from "templates/user_details_modal.js";
 import xss from "xss/dist/xss";
 
 
@@ -130,6 +129,7 @@ converse.plugins.add('converse-chatview', {
 
 
         _converse.UserDetailsModal = _converse.BootstrapModal.extend({
+            id: "user-details-modal",
 
             events: {
                 'click button.remove-contact': 'removeContact',
@@ -157,7 +157,6 @@ converse.plugins.add('converse-chatview', {
                 return tpl_user_details_modal(Object.assign(
                     this.model.toJSON(),
                     vcard_json, {
-                    '__': __,
                     'view': this,
                     '_converse': _converse,
                     'allow_contact_removal': _converse.allow_contact_removal,
@@ -616,7 +615,7 @@ converse.plugins.add('converse-chatview', {
              * content area of the chat box.
              * @private
              * @method _converse.ChatBoxView#insertMessage
-             * @param { Backbone.View } message - The message Backbone.View
+             * @param { View } message - The message View
              */
             insertMessage (view) {
                 if (view.model.get('type') === 'error') {
@@ -1386,8 +1385,7 @@ converse.plugins.add('converse-chatview', {
                   * Get the view of an already open chat.
                   * @method _converse.api.chatviews.get
                   * @param { Array.string | string } jids
-                  * @returns {ChatBoxView} A [Backbone.View](http://backbonejs.org/#View) instance.
-                  *     The chat should already be open, otherwise `undefined` will be returned.
+                  * @returns { _converse.ChatBoxView|undefined }  The chat should already be open, otherwise `undefined` will be returned.
                   * @example
                   * // To return a single view, provide the JID of the contact:
                   * _converse.api.chatviews.get('buddy@example.com')

+ 3 - 6
src/converse-message-view.js

@@ -17,7 +17,7 @@ import tpl_csn from "templates/csn.html";
 import tpl_file_progress from "templates/file_progress.html";
 import tpl_info from "templates/info.html";
 import tpl_message from "templates/message.html";
-import tpl_message_versions_modal from "templates/message_versions_modal.html";
+import tpl_message_versions_modal from "templates/message_versions_modal.js";
 import tpl_spinner from "templates/spinner.html";
 import xss from "xss/dist/xss";
 
@@ -73,12 +73,9 @@ converse.plugins.add('converse-message-view', {
         });
 
         _converse.MessageVersionsModal = _converse.BootstrapModal.extend({
+            id: "message-versions-modal",
             toHTML () {
-                return tpl_message_versions_modal(Object.assign(
-                    this.model.toJSON(), {
-                    '__': __,
-                    'dayjs': dayjs
-                }));
+                return tpl_message_versions_modal(this.model.toJSON());
             }
         });
 

+ 1 - 1
src/converse-minimize.js

@@ -16,7 +16,7 @@ import tpl_chats_panel from "templates/chats_panel.html";
 import tpl_toggle_chats from "templates/toggle_chats.html";
 import tpl_trimmed_chat from "templates/trimmed_chat.html";
 
-const { _ , Backbone, dayjs } = converse.env;
+const { _ , dayjs } = converse.env;
 const u = converse.env.utils;
 
 

+ 35 - 29
src/converse-modal.js

@@ -1,21 +1,20 @@
-// Converse.js
-// https://conversejs.org
-//
-// Copyright (c) 2013-2019, the Converse.js developers
-// Licensed under the Mozilla Public License (MPLv2)
 /**
  * @module converse-modal
+ * @copyright The Converse.js developers
+ * @license Mozilla Public License (MPLv2)
  */
-import "backbone.vdomview";
-import bootstrap from "bootstrap.native";
-import converse from "@converse/headless/converse-core";
+import { HTMLView } from 'skeletor.js/src/htmlview.js';
 import { Model } from 'skeletor.js/src/model.js';
 import { isString } from "lodash";
-import tpl_alert from "templates/alert.html";
-import tpl_alert_modal from "templates/alert_modal.html";
-import tpl_prompt from "templates/prompt.html";
+import { render } from 'lit-html';
+import bootstrap from "bootstrap.native";
+import converse from "@converse/headless/converse-core";
+import log from "@converse/headless/log";
+import tpl_alert_component from "templates/alert.js";
+import tpl_alert_modal from "templates/alert_modal.js";
+import tpl_prompt from "templates/prompt.js";
 
-const { Backbone, sizzle } = converse.env;
+const { sizzle } = converse.env;
 const u = converse.env.utils;
 
 
@@ -25,15 +24,24 @@ converse.plugins.add('converse-modal', {
         const { _converse } = this;
         const { __ } = _converse;
 
-        _converse.BootstrapModal = Backbone.VDOMView.extend({
-
+        _converse.BootstrapModal = HTMLView.extend({
+            className: "modal",
             events: {
                 'click  .nav-item .nav-link': 'switchTab'
             },
 
             initialize () {
-                this.render().insertIntoDOM();
-                this.modal = new bootstrap.Modal(this.el, {
+                this.render()
+
+                this.el.setAttribute('tabindex', '-1');
+                this.el.setAttribute('role', 'dialog');
+                this.el.setAttribute('aria-hidden', 'true');
+                const label_id = this.el.querySelector('.modal-title').getAttribute('id');
+                label_id && this.el.setAttribute('aria-labelledby', label_id);
+
+                this.insertIntoDOM();
+                const Modal = bootstrap.Modal;
+                this.modal = new Modal(this.el, {
                     backdrop: 'static',
                     keyboard: true
                 });
@@ -57,14 +65,14 @@ converse.plugins.add('converse-modal', {
             },
 
             alert (message, type='primary') {
-                const body = this.el.querySelector('.modal-body');
-                body.insertAdjacentHTML(
-                    'afterBegin',
-                    tpl_alert({
-                        'type': `alert-${type}`,
-                        'message': message
-                    })
-                );
+                const body = this.el.querySelector('.modal-alert');
+                if (body === null) {
+                    log.error("Could not find a .modal-alert element in the modal to show an alert message in!");
+                    return;
+                }
+                // FIXME: Instead of adding the alert imperatively, we should
+                // find a way to let the modal rerender with an alert message
+                render(tpl_alert_component({'type': `alert-${type}`, 'message': message}), body);
                 const el = body.firstElementChild;
                 setTimeout(() => {
                     u.addClass('fade-out', el);
@@ -95,7 +103,7 @@ converse.plugins.add('converse-modal', {
             },
 
             toHTML () {
-                return tpl_prompt(Object.assign({__}, this.model.toJSON()));
+                return tpl_prompt(this.model.toJSON());
             },
 
             afterRender () {
@@ -119,7 +127,7 @@ converse.plugins.add('converse-modal', {
 
         _converse.Prompt = _converse.Confirm.extend({
             toHTML () {
-                return tpl_prompt(Object.assign({__}, this.model.toJSON()));
+                return tpl_prompt(this.model.toJSON());
             },
 
             onConfimation (ev) {
@@ -138,8 +146,7 @@ converse.plugins.add('converse-modal', {
             },
 
             toHTML () {
-                return tpl_alert_modal(
-                    Object.assign({__}, this.model.toJSON()));
+                return tpl_alert_modal(Object.assign({__}, this.model.toJSON()));
             }
         });
 
@@ -158,7 +165,6 @@ converse.plugins.add('converse-modal', {
         let alert, prompt, confirm;
 
         Object.assign(_converse.api, {
-
             /**
              * Show a confirm modal to the user.
              * @method _converse.api.confirm

+ 8 - 12
src/converse-muc-views.js

@@ -14,12 +14,12 @@ import { OrderedListView } from "skeletor.js/src/overview";
 import { View } from "skeletor.js/src/view";
 import converse from "@converse/headless/converse-core";
 import log from "@converse/headless/log";
-import tpl_add_chatroom_modal from "templates/add_chatroom_modal.html";
+import tpl_add_chatroom_modal from "templates/add_chatroom_modal.js";
 import tpl_chatarea from "templates/chatarea.html";
 import tpl_chatroom from "templates/chatroom.html";
 import tpl_chatroom_bottom_panel from "templates/chatroom_bottom_panel.html";
 import tpl_chatroom_destroyed from "templates/chatroom_destroyed.html";
-import tpl_chatroom_details_modal from "templates/chatroom_details_modal.html";
+import tpl_chatroom_details_modal from "templates/chatroom_details_modal.js";
 import tpl_chatroom_disconnect from "templates/chatroom_disconnect.html";
 import tpl_chatroom_features from "templates/chatroom_features.html";
 import tpl_chatroom_form from "templates/chatroom_form.html";
@@ -29,8 +29,8 @@ import tpl_chatroom_nickname_form from "templates/chatroom_nickname_form.html";
 import tpl_chatroom_password_form from "templates/chatroom_password_form.html";
 import tpl_chatroom_sidebar from "templates/chatroom_sidebar.html";
 import tpl_info from "templates/info.html";
-import tpl_list_chatrooms_modal from "templates/list_chatrooms_modal.html";
-import tpl_moderator_tools_modal from "templates/moderator_tools_modal.html";
+import tpl_list_chatrooms_modal from "templates/list_chatrooms_modal.js";
+import tpl_moderator_tools_modal from "templates/moderator_tools_modal.js";
 import tpl_occupant from "templates/occupant.html";
 import tpl_room_description from "templates/room_description.html";
 import tpl_room_item from "templates/room_item.html";
@@ -226,7 +226,7 @@ converse.plugins.add('converse-muc-views', {
 
 
         _converse.ModeratorToolsModal = _converse.BootstrapModal.extend({
-
+            id: "converse-modtools-modal",
             events: {
                 'submit .affiliation-form': 'assignAffiliation',
                 'submit .role-form': 'assignRole',
@@ -271,7 +271,6 @@ converse.plugins.add('converse-muc-views', {
                 allowed_roles.sort();
 
                 return tpl_moderator_tools_modal(Object.assign(this.model.toJSON(), {
-                    __,
                     allowed_affiliations,
                     allowed_roles,
                     'affiliations': [...AFFILIATIONS, 'none'],
@@ -389,6 +388,7 @@ converse.plugins.add('converse-muc-views', {
 
 
         _converse.ListChatRoomsModal = _converse.BootstrapModal.extend({
+            id: "list-chatrooms-modal",
 
             events: {
                 'submit form': 'showRooms',
@@ -409,9 +409,6 @@ converse.plugins.add('converse-muc-views', {
             toHTML () {
                 const muc_domain = this.model.get('muc_domain') || _converse.muc_domain;
                 return tpl_list_chatrooms_modal(Object.assign(this.model.toJSON(), {
-                    'heading_list_chatrooms': __('Query for Groupchats'),
-                    'label_server_address': __('Server address'),
-                    'label_query': __('Show groupchats'),
                     'show_form': !_converse.locked_muc_domain,
                     'server_placeholder': muc_domain ? muc_domain : __('conference.example.org')
                 }));
@@ -523,6 +520,7 @@ converse.plugins.add('converse-muc-views', {
 
 
         _converse.AddChatRoomModal = _converse.BootstrapModal.extend({
+            id: 'add-chatroom-modal',
 
             events: {
                 'submit form.add-chatroom': 'openChatRoom',
@@ -543,7 +541,6 @@ converse.plugins.add('converse-muc-views', {
                     placeholder = muc_domain ? `name@${muc_domain}` : __('name@conference.example.org');
                 }
                 return tpl_add_chatroom_modal(Object.assign(this.model.toJSON(), {
-                    '__': _converse.__,
                     '_converse': _converse,
                     'label_room_address': _converse.muc_domain ? __('Groupchat name') :  __('Groupchat address'),
                     'chatroom_placeholder': placeholder,
@@ -616,6 +613,7 @@ converse.plugins.add('converse-muc-views', {
 
 
         _converse.RoomDetailsModal = _converse.BootstrapModal.extend({
+            id: "room-details-modal",
 
             initialize () {
                 _converse.BootstrapModal.prototype.initialize.apply(this, arguments);
@@ -627,12 +625,10 @@ converse.plugins.add('converse-muc-views', {
             toHTML () {
                 return tpl_chatroom_details_modal(Object.assign(
                     this.model.toJSON(), {
-                        '__': __,
                         'config': this.model.config.toJSON(),
                         'display_name': __('Groupchat info for %1$s', this.model.getDisplayName()),
                         'features': this.model.features.toJSON(),
                         'num_occupants': this.model.occupants.length,
-                        'topic': u.addHyperlinks(xss.filterXSS(get(this.model.get('subject'), 'text'), {'whiteList': {}}))
                     })
                 );
             }

+ 18 - 52
src/converse-profile.js

@@ -1,11 +1,7 @@
-// Converse.js (A browser based XMPP chat client)
-// https://conversejs.org
-//
-// Copyright (c) 2013-2017, Jan-Carel Brand <jc@opkode.com>
-// Licensed under the Mozilla Public License (MPLv2)
-//
 /**
  * @module converse-profile
+ * @copyright The Converse.js developers
+ * @license Mozilla Public License (MPLv2)
  */
 import "@converse/headless/converse-status";
 import "@converse/headless/converse-vcard";
@@ -14,12 +10,12 @@ import "formdata-polyfill";
 import bootstrap from "bootstrap.native";
 import converse from "@converse/headless/converse-core";
 import log from "@converse/headless/log";
-import tpl_chat_status_modal from "templates/chat_status_modal.html";
-import tpl_client_info_modal from "templates/client_info_modal.html";
-import tpl_profile_modal from "templates/profile_modal.html";
-import tpl_profile_view from "templates/profile_view.html";
+import sizzle from 'sizzle';
+import tpl_chat_status_modal from "templates/chat_status_modal";
+import tpl_client_info_modal from "templates/client_info_modal";
+import tpl_profile from "templates/profile.js";
+import tpl_profile_modal from "templates/profile_modal";
 
-const { sizzle } = converse.env;
 const u = converse.env.utils;
 
 
@@ -40,6 +36,7 @@ converse.plugins.add('converse-profile', {
 
 
         _converse.ProfileModal = _converse.BootstrapModal.extend({
+            id: "user-profile-modal",
             events: {
                 'change input[type="file"': "updateFilePreview",
                 'click .change-avatar': "openFileSelection",
@@ -62,20 +59,7 @@ converse.plugins.add('converse-profile', {
                 return tpl_profile_modal(Object.assign(
                     this.model.toJSON(),
                     this.model.vcard.toJSON(), {
-                    '__': __,
                     '_converse': _converse,
-                    'alt_avatar': __('Your avatar image'),
-                    'heading_profile': __('Your Profile'),
-                    'label_close': __('Close'),
-                    'label_email': __('Email'),
-                    'label_fullname': __('Full Name'),
-                    'label_jid': __('XMPP Address (JID)'),
-                    'label_nickname': __('Nickname'),
-                    'label_role': __('Role'),
-                    'label_role_help': __(
-                        'Use commas to separate multiple roles. '+
-                        'Your roles are shown next to your name on your chat messages.'),
-                    'label_url': __('URL'),
                     'utils': u,
                     'view': this
                 }));
@@ -146,6 +130,7 @@ converse.plugins.add('converse-profile', {
 
 
         _converse.ChatStatusModal = _converse.BootstrapModal.extend({
+            id: "modal-status-change",
             events: {
                 "submit form#set-xmpp-status": "onFormSubmitted",
                 "click .clear-input": "clearStatusMessage"
@@ -157,9 +142,9 @@ converse.plugins.add('converse-profile', {
                         this.model.toJSON(),
                         this.model.vcard.toJSON(), {
                         'label_away': __('Away'),
-                        'label_close': __('Close'),
                         'label_busy': __('Busy'),
                         'label_cancel': __('Cancel'),
+                        'label_close': __('Close'),
                         'label_custom_status': __('Custom status'),
                         'label_offline': __('Offline'),
                         'label_online': __('Online'),
@@ -197,31 +182,20 @@ converse.plugins.add('converse-profile', {
         });
 
         _converse.ClientInfoModal = _converse.BootstrapModal.extend({
+            id: "converse-client-info-modal",
 
             toHTML () {
                 return tpl_client_info_modal(
                     Object.assign(
                         this.model.toJSON(),
-                        this.model.vcard.toJSON(), {
-                            '__': __,
-                            'modal_title': __('About'),
-                            'version_name': _converse.VERSION_NAME,
-                            'first_subtitle': __( '%1$s Open Source %2$s XMPP chat client brought to you by %3$s Opkode %2$s',
-                                '<a target="_blank" rel="nofollow" href="https://conversejs.org">',
-                                '</a>',
-                                '<a target="_blank" rel="nofollow" href="https://opkode.com">'
-                            ),
-                            'second_subtitle': __('%1$s Translate %2$s it into your own language',
-                                '<a target="_blank" rel="nofollow" href="https://hosted.weblate.org/projects/conversejs/#languages">',
-                                '</a>'
-                            )
-                        }
+                        this.model.vcard.toJSON(),
+                        { 'version_name': _converse.VERSION_NAME }
                     )
                 );
             }
         });
 
-        _converse.XMPPStatusView = _converse.VDOMViewWithAvatar.extend({
+        _converse.XMPPStatusView = _converse.HTMLViewWithAvatar.extend({
             tagName: "div",
             events: {
                 "click a.show-profile": "showProfileModal",
@@ -237,20 +211,14 @@ converse.plugins.add('converse-profile', {
 
             toHTML () {
                 const chat_status = this.model.get('status') || 'offline';
-                return tpl_profile_view(Object.assign(
+                return tpl_profile(Object.assign(
                     this.model.toJSON(),
                     this.model.vcard.toJSON(), {
-                    '__': __,
+                    _converse,
+                    chat_status,
                     'fullname': this.model.vcard.get('fullname') || _converse.bare_jid,
                     'status_message': this.model.get('status_message') ||
-                                        __("I am %1$s", this.getPrettyStatus(chat_status)),
-                    'chat_status': chat_status,
-                    '_converse': _converse,
-                    'title_change_settings': __('Change settings'),
-                    'title_change_status': __('Click to change your chat status'),
-                    'title_log_out': __('Log out'),
-                    'info_details': __('Show details about this chat client'),
-                    'title_your_profile': __('Your profile')
+                                        __("I am %1$s", this.getPrettyStatus(chat_status))
                 }));
             },
 
@@ -306,7 +274,6 @@ converse.plugins.add('converse-profile', {
 
 
         /******************** Event Handlers ********************/
-
         _converse.api.listen.on('controlBoxPaneInitialized', async view => {
             await _converse.api.waitUntil('VCardsInitialized');
             _converse.xmppstatusview = new _converse.XMPPStatusView({'model': _converse.xmppstatus});
@@ -314,4 +281,3 @@ converse.plugins.add('converse-profile', {
         });
     }
 });
-

+ 4 - 11
src/converse-rosterview.js

@@ -1,10 +1,7 @@
-// Converse.js
-// https://conversejs.org
-//
-// Copyright (c) 2013-2019, the Converse.js developers
-// Licensed under the Mozilla Public License (MPLv2)
 /**
  * @module converse-rosterview
+ * @copyright 2013-2019, the Converse.js developers
+ * @license Mozilla Public License (MPLv2)
  */
 import "@converse/headless/converse-chatboxes";
 import "@converse/headless/converse-roster";
@@ -16,7 +13,7 @@ import { OrderedListView } from "skeletor.js/src/overview";
 import SHA1 from 'strophe.js/src/sha1';
 import converse from "@converse/headless/converse-core";
 import log from "@converse/headless/log";
-import tpl_add_contact_modal from "templates/add_contact_modal.html";
+import tpl_add_contact_modal from "templates/add_contact_modal.js";
 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";
@@ -61,6 +58,7 @@ converse.plugins.add('converse-rosterview', {
 
 
         _converse.AddContactModal = _converse.BootstrapModal.extend({
+            id: "add-contact-modal",
             events: {
                 'submit form': 'addContactFromForm'
             },
@@ -74,12 +72,7 @@ converse.plugins.add('converse-rosterview', {
                 const label_nickname = _converse.xhr_user_search_url ? __('Contact name') : __('Optional nickname');
                 return tpl_add_contact_modal(Object.assign(this.model.toJSON(), {
                     '_converse': _converse,
-                    'heading_new_contact': __('Add a Contact'),
-                    'label_xmpp_address': __('XMPP Address'),
                     'label_nickname': label_nickname,
-                    'contact_placeholder': __('name@example.org'),
-                    'label_add': __('Add'),
-                    'error_message': __('Please enter a valid XMPP address')
                 }));
             },
 

+ 6 - 11
src/headless/converse-core.js

@@ -3,8 +3,10 @@
  * @copyright The Converse.js developers
  * @license Mozilla Public License (MPLv2)
  */
+import { __, i18n } from './i18n';
 import { assignIn, debounce, get, invoke, isFunction, isObject, isString, pick } from 'lodash';
 import { Collection } from "skeletor.js/src/collection";
+import { Events } from 'skeletor.js/src/events.js';
 import { Model } from 'skeletor.js/src/model.js';
 import { Router } from 'skeletor.js/src/router.js';
 import 'strophe.js/src/websocket';
@@ -15,7 +17,6 @@ import Backbone from 'backbone';
 import Storage from 'skeletor.js/src/storage.js';
 import advancedFormat from 'dayjs/plugin/advancedFormat';
 import dayjs from 'dayjs';
-import i18n from './i18n';
 import log from '@converse/headless/log';
 import pluggable from 'pluggable.js/src/pluggable';
 import sizzle from 'sizzle';
@@ -114,7 +115,7 @@ const _converse = {
 
 _converse.VERSION_NAME = "v6.0.1dev";
 
-Object.assign(_converse, Backbone.Events);
+Object.assign(_converse, Events);
 
 _converse.router = new Router();
 
@@ -258,12 +259,7 @@ _converse.default_settings = {
  * @memberOf _converse
  * @param { String } str - The string to translate
  */
-_converse.__ = function (str) {
-    if (i18n === undefined) {
-        return str;
-    }
-    return i18n.translate.apply(i18n, arguments);
-};
+_converse.__ = __;
 
 
 /**
@@ -287,8 +283,6 @@ _converse.___ = function (str) {
 }
 
 
-const __ = _converse.__;
-
 const PROMISES = [
     'afterResourceBinding',
     'connectionInitialized',
@@ -1019,7 +1013,7 @@ _converse.initialize = async function (settings, callback) {
     );
 
     /* Localisation */
-    if (i18n === undefined || _converse.isTestEnv()) {
+    if (_converse.isTestEnv()) {
         _converse.locale = 'en';
     } else {
         try {
@@ -1027,6 +1021,7 @@ _converse.initialize = async function (settings, callback) {
             await i18n.fetchTranslations(_converse);
         } catch (e) {
             log.fatal(e.message);
+            _converse.locale = 'en';
         }
     }
 

+ 6 - 1
src/headless/i18n.js

@@ -70,7 +70,7 @@ let jed_instance;
 /**
  * @namespace i18n
  */
-export default {
+export const i18n = {
 
     getLocale (preferred_locale, available_locales) {
         return getLocale(preferred_locale, preferred => isConverseLocale(preferred, available_locales));
@@ -105,3 +105,8 @@ export default {
         jed_instance = new Jed(data);
     }
 };
+
+
+export const __ = function () {
+    return i18n.translate.apply(i18n, arguments);
+}

+ 1 - 1
src/headless/package.json

@@ -26,7 +26,7 @@
   },
   "gitHead": "9641dcdc820e029b05930479c242d2b707bbe8e2",
   "devDependencies": {
-    "skeletor.js": "skeletorjs/skeletor#abc4e9d25d30159e9cffc14bf5f7ffe17b3665eb",
+    "skeletor.js": "skeletorjs/skeletor#29a6d8f707076e865133b8f36f07c76ba4b4b582",
     "backbone": "1.4",
     "backbone.browserStorage": "conversejs/backbone.browserStorage#674ba3aa0e4d0f0b0dcac48fcc7dea531012828f",
     "filesize": "^4.1.2",

+ 4 - 4
src/headless/polyfill.js

@@ -1,6 +1,6 @@
 function CustomEvent ( event, params ) {
     params = params || { bubbles: false, cancelable: false, detail: undefined };
-    var evt = document.createEvent( 'CustomEvent' );
+    const evt = document.createEvent( 'CustomEvent' );
     evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
     return evt;
 }
@@ -25,12 +25,12 @@ if (!String.prototype.includes) {
 
 if (!String.prototype.endsWith) {
   String.prototype.endsWith = function (searchString, position) {
-      var subjectString = this.toString();
+      const subjectString = this.toString();
       if (position === undefined || position > subjectString.length) {
           position = subjectString.length;
       }
       position -= searchString.length;
-      var lastIndex = subjectString.indexOf(searchString, position);
+      const lastIndex = subjectString.indexOf(searchString, position);
       return lastIndex !== -1 && lastIndex === position;
   };
 }
@@ -44,7 +44,7 @@ if (!String.prototype.startsWith) {
 
 if (!String.prototype.splitOnce) {
     String.prototype.splitOnce = function (delimiter) {
-        var components = this.split(delimiter);
+        const components = this.split(delimiter);
         return [components.shift(), components.join(delimiter)];
     };
 }

+ 0 - 36
src/templates/add_chatroom_modal.html

@@ -1,36 +0,0 @@
-<div class="modal" 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-content">
-            <div class="modal-header">
-                <h5 class="modal-title"
-                    id="add-chatroom-modal-label">{{{o.__('Enter a new Groupchat')}}}</h5>
-                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
-                    <span aria-hidden="true">×</span>
-                </button>
-            </div>
-            <div class="modal-body">
-                <form class="converse-form add-chatroom">
-                    <div class="form-group">
-                        <label for="chatroom">{{{o.label_room_address}}}:</label>
-                        {[ if (o.muc_roomid_policy_error_msg) { ]}
-                            <label class="roomid-policy-error">{{{o.muc_roomid_policy_error_msg}}}</label>
-                        {[ } ]}
-                        <input type="text" required="required" name="chatroom" class="form-control roomjid-input" placeholder="{{{o.chatroom_placeholder}}}"/>
-                    </div>
-                    {[ if (o.muc_roomid_policy_hint) { ]}
-                        <div class="form-group">
-                            {{o.muc_roomid_policy_hint}}
-                        </div>
-                    {[ } ]}
-                    {[ if (!o._converse.locked_muc_nickname) { ]}
-                    <div class="form-group" >
-                        <label for="nickname">{{{o.__('Nickname')}}}:</label>
-                        <input type="text" pattern=".*\S+.*" title="{{{o.__('This field is required')}}}" required="required" name="nickname" value="{{{o.nick}}}" class="form-control"/>
-                    </div>
-                    {[ } ]}
-                    <input type="submit" class="btn btn-primary" name="join" value="{{{o.__('Join')}}}" {[ if (o.muc_roomid_policy_error_msg) { ]} disabled=true {[ } ]}/>
-                </form>
-            </div>
-        </div>
-    </div>
-</div>

+ 42 - 0
src/templates/add_chatroom_modal.js

@@ -0,0 +1,42 @@
+import { html } from "lit-html";
+import { __ } from '@converse/headless/i18n';
+import { modal_header_close_button } from "./buttons"
+
+
+const i18n_join = __('Join');
+const i18n_enter = __('Enter a new Groupchat');
+const i18n_nickname = __('Nickname');
+const i18n_required_field = __('This field is required');
+
+
+const nickname_input = (o) => html`
+    <div class="form-group" >
+        <label for="nickname">${i18n_nickname}:</label>
+        <input type="text" title="${i18n_required_field}" required="required" name="nickname" value="${o.nick || ''}" class="form-control"/>
+    </div>
+`;
+
+
+export default (o) => html`
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title" id="add-chatroom-modal-label">${i18n_enter}</h5>
+                ${modal_header_close_button}
+            </div>
+            <div class="modal-body">
+                <span class="modal-alert"></span>
+                <form class="converse-form add-chatroom">
+                    <div class="form-group">
+                        <label for="chatroom">${o.label_room_address}:</label>
+                        ${ (o.muc_roomid_policy_error_msg) ? html`<label class="roomid-policy-error">${o.muc_roomid_policy_error_msg}</label>` : '' }
+                        <input type="text" required="required" name="chatroom" class="form-control roomjid-input" placeholder="${o.chatroom_placeholder}"/>
+                    </div>
+                    ${ o.muc_roomid_policy_hint ?  html`<div class="form-group">{{o.muc_roomid_policy_hint}}</div>` : '' }
+                    ${ !o._converse.locked_muc_nickname ? nickname_input(o) : '' }
+                    <input type="submit" class="btn btn-primary" name="join" value="${i18n_join || ''}" ?disabled=${o.muc_roomid_policy_error_msg}>
+                </form>
+            </div>
+        </div>
+    </div>
+`;

+ 28 - 17
src/templates/add_contact_modal.html → src/templates/add_contact_modal.js

@@ -1,41 +1,52 @@
-<!-- Add contact Modal -->
-<div class="modal" id="add-contact-modal" tabindex="-1" role="dialog" aria-labelledby="addContactModalLabel" aria-hidden="true">
+import { html } from "lit-html";
+import { __ } from '@converse/headless/i18n';
+import { modal_header_close_button } from "./buttons"
+
+const i18n_contact_placeholder = __('name@example.org');
+const i18n_add = __('Add');
+const i18n_error_message = __('Please enter a valid XMPP address');
+const i18n_new_contact = __('Add a Contact');
+const i18n_xmpp_address = __('XMPP Address');
+const i18n_nickname = __('Nickname');
+
+
+export default (o) => html`
     <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">×</span></button>
+                <h5 class="modal-title" id="addContactModalLabel">${i18n_new_contact}</h5>
+                ${modal_header_close_button}
             </div>
             <form class="converse-form add-xmpp-contact">
                 <div class="modal-body">
+                    <span class="modal-alert"></span>
                     <div class="form-group add-xmpp-contact__jid">
-                        <label class="clearfix" for="jid">{{{o.label_xmpp_address}}}:</label>
+                        <label class="clearfix" for="jid">${i18n_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"
-                                   {[ if (!o._converse.xhr_user_search_url) { ]} required="required" {[ } ]}
-                                   value="{{{o.jid}}}"
-                                   class="form-control suggestion-box__input"
-                                   placeholder="{{{o.contact_placeholder}}}"/>
+                            <input type="text" name="jid" ?required=${(!o._converse.xhr_user_search_url)}
+                                value="${o.jid || ''}"
+                                class="form-control suggestion-box__input"
+                                placeholder="${i18n_contact_placeholder}"/>
                             <span class="suggestion-box__additions visually-hidden" role="status" aria-live="assertive" aria-relevant="additions"></span>
                         </div>
                     </div>
                     <div class="form-group add-xmpp-contact__name">
-                        <label class="clearfix" for="name">{{{o.label_nickname}}}:</label>
+                        <label class="clearfix" for="name">${i18n_nickname}:</label>
                         <div class="suggestion-box suggestion-box__name">
                             <ul class="suggestion-box__results suggestion-box__results--above" hidden=""></ul>
-                            <input type="text" name="name" value="{{{o.nickname}}}"
-                                   class="form-control suggestion-box__input"
-                                   placeholder="{{{o.nickname_placeholder}}}"/>
+                            <input type="text" name="name" value="${o.nickname || ''}"
+                                class="form-control suggestion-box__input"
+                                placeholder="${i18n_nickname}"/>
                             <span class="suggestion-box__additions visually-hidden" role="status" aria-live="assertive" aria-relevant="additions"></span>
                         </div>
                     </div>
                     <div class="form-group">
-                        <div class="invalid-feedback">{{{o.error_message}}}</div>
+                        <div class="invalid-feedback">${i18n_error_message}</div>
                     </div>
-                    <button type="submit" class="btn btn-primary">{{{o.label_add}}}</button>
+                    <button type="submit" class="btn btn-primary">${i18n_add}</button>
                 </div>
             </form>
         </div>
     </div>
-</div>
+`;

+ 0 - 1
src/templates/alert.html

@@ -1 +0,0 @@
-<div class="alert {{{o.type}}}" role="alert"><p>{{{o.message}}}</p></div>

+ 3 - 0
src/templates/alert.js

@@ -0,0 +1,3 @@
+import { html } from "lit-html";
+
+export default (o) => html`<div class="alert ${o.type}" role="alert"><p>${o.message}</p></div>`

+ 0 - 16
src/templates/alert_modal.html

@@ -1,16 +0,0 @@
-<div class="modal" tabindex="-1" role="dialog">
-  <div class="modal-dialog" role="document">
-    <div class="modal-content">
-      <div class="modal-header {{{o.level}}}">
-        <h5 class="modal-title">{{{o.title}}}</h5>
-        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
-          <span aria-hidden="true">×</span>
-        </button>
-      </div>
-      <div class="modal-body">{[o.messages.forEach(function (message) { ]}
-          <p>{{{message}}}</p>
-      {[ }) ]}
-      </div>
-    </div>
-  </div>
-</div>

+ 18 - 0
src/templates/alert_modal.js

@@ -0,0 +1,18 @@
+import { html } from "lit-html";
+import { modal_header_close_button } from "./buttons"
+
+
+export default (o) => html`
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header ${o.level}">
+              <h5 class="modal-title">${o.title}</h5>
+              ${modal_header_close_button}
+            </div>
+            <div class="modal-body">
+              <span class="modal-alert"></span>
+              ${ o.messages.map(message => html`<p>${message}</p>`) }
+            </div>
+        </div>
+    </div>
+`;

+ 5 - 0
src/templates/avatar.js

@@ -0,0 +1,5 @@
+import { html } from "lit-html";
+
+export default  (o) => html`
+    <img alt="${o.alt_text}" class="img-thumbnail avatar align-self-center ${o.extra_classes}"
+            height="100px" width="100px" src="data:${o.image_type};base64,${o.image}"/>`;

+ 9 - 0
src/templates/buttons.js

@@ -0,0 +1,9 @@
+import { __ } from '@converse/headless/i18n';
+import { html } from "lit-html";
+
+const i18n_close = __('Close');
+
+export const modal_close_button = html`<button type="button" class="btn btn-secondary" data-dismiss="modal">${i18n_close}</button>`;
+
+export const modal_header_close_button = html`<button type="button" class="close" data-dismiss="modal" aria-label="${i18n_close}"><span aria-hidden="true">×</span></button>`;
+

+ 25 - 23
src/templates/chat_status_modal.html → src/templates/chat_status_modal.js

@@ -1,51 +1,53 @@
-<!-- Change status Modal -->
-<div class="modal" id="modal-status-change" tabindex="-1" role="dialog" aria-labelledby="changeStatusModalLabel" aria-hidden="true">
+import { html } from "lit-html";
+import { modal_header_close_button } from "./buttons"
+
+
+export default (o) => html`
     <div class="modal-dialog" role="document">
         <div class="modal-content">
             <div class="modal-header">
-                <h5 class="modal-title" id="changeStatusModalLabel">{{{o.modal_title}}}</h5>
-                <button type="button" class="close" data-dismiss="modal" aria-label="{{{o.label_close}}}">
-                    <span aria-hidden="true">×</span>
-                </button>
+                <h5 class="modal-title" id="changeStatusModalLabel">${o.modal_title}</h5>
+                ${modal_header_close_button}
             </div>
             <div class="modal-body">
+                <span class="modal-alert"></span>
                 <form class="converse-form set-xmpp-status" id="set-xmpp-status">
                     <div class="form-group">
                         <div class="custom-control custom-radio">
-                            <input {[ if (o.status === 'online') { ]} checked="checked" {[ } ]}
-                                   type="radio" id="radio-online" value="online" name="chat_status" class="custom-control-input"/>
+                            <input ?checked=${o.status === 'online'}
+                                type="radio" id="radio-online" value="online" name="chat_status" class="custom-control-input"/>
                             <label class="custom-control-label" for="radio-online">
-                                <span class="fa fa-circle chat-status chat-status--online"></span>{{{o.label_online}}}</label>
+                                <span class="fa fa-circle chat-status chat-status--online"></span>${o.label_online}</label>
                         </div>
                         <div class="custom-control custom-radio">
-                            <input {[ if (o.status === 'busy') { ]} checked="checked" {[ } ]}
-                                   type="radio" id="radio-busy" value="dnd" name="chat_status" class="custom-control-input"/>
+                            <input ?checked=${o.status === 'busy'}
+                                type="radio" id="radio-busy" value="dnd" name="chat_status" class="custom-control-input"/>
                             <label class="custom-control-label" for="radio-busy">
-                                <span class="fa fa-minus-circle  chat-status chat-status--busy"></span>{{{o.label_busy}}}</label>
+                                <span class="fa fa-minus-circle  chat-status chat-status--busy"></span>${o.label_busy}</label>
                         </div>
                         <div class="custom-control custom-radio">
-                            <input {[ if (o.status === 'away') { ]} checked="checked" {[ } ]}
-                                   type="radio" id="radio-away" value="away" name="chat_status" class="custom-control-input"/>
+                            <input ?checked=${o.status === 'away'}
+                                type="radio" id="radio-away" value="away" name="chat_status" class="custom-control-input"/>
                             <label class="custom-control-label" for="radio-away">
-                                <span class="fa fa-circle chat-status chat-status--away"></span>{{{o.label_away}}}</label>
+                                <span class="fa fa-circle chat-status chat-status--away"></span>${o.label_away}</label>
                         </div>
                         <div class="custom-control custom-radio">
-                            <input {[ if (o.status === 'xa') { ]} checked="checked" {[ } ]}
-                                   type="radio" id="radio-xa" value="xa" name="chat_status" class="custom-control-input"/>
+                            <input ?checked=${o.status === 'xa'}
+                                type="radio" id="radio-xa" value="xa" name="chat_status" class="custom-control-input"/>
                             <label class="custom-control-label" for="radio-xa">
-                                <span class="far fa-circle chat-status chat-status--xa"></span>{{{o.label_xa}}}</label>
+                                <span class="far fa-circle chat-status chat-status--xa"></span>${o.label_xa}</label>
                         </div>
                     </div>
                     <div class="form-group">
                         <div class="btn-group w-100">
-                            <input name="status_message" type="text" class="form-control" 
-                                value="{{{o.status_message}}}" placeholder="{{{o.placeholder_status_message}}}"/>
-                            <span class="clear-input fa fa-times {[ if (!o.status_message) { ]} hidden {[ } ]}"></span>
+                            <input name="status_message" type="text" class="form-control"
+                                value="${o.status_message || ''}" placeholder="${o.placeholder_status_message}"/>
+                            <span class="clear-input fa fa-times ${o.status_message ? '' : 'hidden'}"></span>
                         </div>
                     </div>
-                    <button type="submit" class="btn btn-primary">{{{o.label_save}}}</button>
+                    <button type="submit" class="btn btn-primary">${o.label_save}</button>
                 </form>
             </div>
         </div>
     </div>
-</div>
+`;

+ 0 - 70
src/templates/chatroom_details_modal.html

@@ -1,70 +0,0 @@
-<div class="modal" id="room-details-modal" tabindex="-1" role="dialog" aria-labelledby="room-details-modal-label" aria-hidden="true">
-    <div class="modal-dialog" role="document">
-        <div class="modal-content">
-            <div class="modal-header">
-                <h5 class="modal-title" id="room-details-modal-label">{{{o.display_name}}}</h5>
-                <button type="button" class="close" data-dismiss="modal" aria-label="{{{o.label_close}}}"><span aria-hidden="true">×</span></button>
-            </div>
-            <div class="modal-body">
-                <div class="room-info">
-                    <p class="room-info"><strong>{{{o.__('Name')}}}</strong>: {{{o.name}}}</p>
-                    <p class="room-info"><strong>{{{o.__('Groupchat address (JID)')}}}</strong>: {{{o.jid}}}</p>
-                    <p class="room-info"><strong>{{{o.__('Description')}}}</strong>: {{{o.config.description}}}</p>
-                    {[ if (o.subject) { ]}
-                    <p class="room-info"><strong>{{{o.__('Topic')}}}</strong>: {{o.topic}}</p> <!-- Sanitized in converse-muc-views. We want to render links. -->
-                        <p class="room-info"><strong>{{{o.__('Topic author')}}}</strong>: {{{o.subject && o.subject.author}}}</p>
-                    {[ } ]}
-                    <p class="room-info"><strong>{{{o.__('Online users')}}}</strong>: {{{o.num_occupants}}}</p>
-                    <p class="room-info"><strong>{{{o.__('Features')}}}</strong>:
-                        <div class="chatroom-features">
-                        <ul class="features-list">
-                        {[ if (o.features.passwordprotected) { ]}
-                        <li class="feature" ><span class="fa fa-lock"></span>{{{ o.__('Password protected') }}} - <em>{{{ o.__('This groupchat requires a password before entry') }}}</em></li>
-                        {[ } ]}
-                        {[ if (o.features.unsecured) { ]}
-                        <li class="feature" ><span class="fa fa-unlock"></span>{{{ o.__('No password required') }}} - <em>{{{ o.__('This groupchat does not require a password upon entry') }}}</em></li>
-                        {[ } ]}
-                        {[ if (o.features.hidden) { ]}
-                        <li class="feature" ><span class="fa fa-eye-slash"></span>{{{ o.__('Hidden') }}} - <em>{{{ o.__('This groupchat is not publicly searchable') }}}</em></li>
-                        {[ } ]}
-                        {[ if (o.features.public_room) { ]}
-                        <li class="feature" ><span class="fa fa-eye"></span>{{{ o.__('Public') }}} - <em>{{{ o.__('This groupchat is publicly searchable') }}}</em></li>
-                        {[ } ]}
-                        {[ if (o.features.membersonly) { ]}
-                        <li class="feature" ><span class="fa fa-address-book"></span>{{{ o.__('Members only') }}} - <em>{{{ o.__('This groupchat is restricted to members only') }}}</em></li>
-                        {[ } ]}
-                        {[ if (o.features.open) { ]}
-                        <li class="feature" ><span class="fa fa-globe"></span>{{{ o.__('Open') }}} - <em>{{{ o.__('Anyone can join this groupchat') }}}</em></li>
-                        {[ } ]}
-                        {[ if (o.features.persistent) { ]}
-                        <li class="feature" ><span class="fa fa-save"></span>{{{ o.__('Persistent') }}} - <em>{{{ o.__('This groupchat persists even if it\'s unoccupied') }}}</em></li>
-                        {[ } ]}
-                        {[ if (o.features.temporary) { ]}
-                        <li class="feature" ><span class="fa fa-snowflake-o"></span>{{{ o.__('Temporary') }}} - <em>{{{ o.__('This groupchat will disappear once the last person leaves') }}}</em></li>
-                        {[ } ]}
-                        {[ if (o.features.nonanonymous) { ]}
-                        <li class="feature" ><span class="fa fa-id-card"></span>{{{ o.__('Not anonymous') }}} - <em>{{{ o.__('All other groupchat participants can see your XMPP address') }}}</em></li>
-                        {[ } ]}
-                        {[ if (o.features.semianonymous) { ]}
-                        <li class="feature" ><span class="fa fa-user-secret"></span>{{{ o.__('Semi-anonymous') }}} - <em>{{{ o.__('Only moderators can see your XMPP address') }}}</em></li>
-                        {[ } ]}
-                        {[ if (o.features.moderated) { ]}
-                        <li class="feature" ><span class="fa fa-gavel"></span>{{{ o.__('Moderated') }}} - <em>{{{ o.__('Participants entering this groupchat need to request permission to write') }}}</em></li>
-                        {[ } ]}
-                        {[ if (o.features.unmoderated) { ]}
-                        <li class="feature" ><span class="fa fa-info-circle"></span>{{{ o.__('Not moderated') }}} - <em>{{{ o.__('Participants entering this groupchat can write right away') }}}</em></li>
-                        {[ } ]}
-                        {[ if (o.features.mam_enabled) { ]}
-                        <li class="feature" ><span class="fa fa-database"></span>{{{ o.__('Message archiving') }}} - <em>{{{ o.__('Messages are archived on the server') }}}</em></li>
-                        {[ } ]}
-                        </ul>
-                        </div>
-                    </p>
-                </div>
-            </div>
-            <div class="modal-footer">
-                <button type="button" class="btn btn-warning" data-dismiss="modal">{{{o.__('Close')}}}</button>
-            </div>
-        </div>
-    </div>
-</div>

+ 88 - 0
src/templates/chatroom_details_modal.js

@@ -0,0 +1,88 @@
+import { __ } from '@converse/headless/i18n';
+import { html } from "lit-html";
+import { modal_close_button, modal_header_close_button } from "./buttons"
+import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
+import xss from "xss/dist/xss";
+
+
+const i18n_address =  __('Groupchat address (JID)');
+const i18n_archiving = __('Message archiving');
+const i18n_archiving_help = __('Messages are archived on the server');
+const i18n_close = __('Close');
+const i18n_desc = __('Description');
+const i18n_features = __('Features');
+const i18n_hidden = __('Hidden');
+const i18n_hidden_help = __('This groupchat is not publicly searchable');
+const i18n_members_help = __('This groupchat is restricted to members only');
+const i18n_members_only = __('Members only');
+const i18n_moderated = __('Moderated');
+const i18n_moderated_help = __('Participants entering this groupchat need to request permission to write');
+const i18n_name = __('Name');
+const i18n_no_pass_help = __('This groupchat does not require a password upon entry');
+const i18n_no_password_required = __('No password required');
+const i18n_not_anonymous = __('Not anonymous');
+const i18n_not_anonymous_help = __('All other groupchat participants can see your XMPP address');
+const i18n_not_moderated = __('Not moderated');
+const i18n_not_moderated_help = __('Participants entering this groupchat can write right away');
+const i18n_online_users = __('Online users');
+const i18n_open = __('Open');
+const i18n_open_help = __('Anyone can join this groupchat');
+const i18n_password_help = __('This groupchat requires a password before entry');
+const i18n_password_protected = __('Password protected');
+const i18n_persistent = __('Persistent');
+const i18n_persistent_help = __('This groupchat persists even if it\'s unoccupied');
+const i18n_public = __('Public');
+const i18n_semi_anon = __('Semi-anonymous');
+const i18n_semi_anon_help = __('Only moderators can see your XMPP address');
+const i18n_temporary = __('Temporary');
+const i18n_temporary_help = __('This groupchat will disappear once the last person leaves');
+const i18n_topic = __('Topic');
+const i18n_topic_author = __('Topic author');
+
+
+const subject = (o) => html`
+    <p class="room-info"><strong>${i18n_topic}</strong>: ${unsafeHTML(xss.filterXSS(o.subject.text, {'whitelist': {}}))}</p>
+        <p class="room-info"><strong>${i18n_topic_author}</strong>: ${o.subject && o.subject.author}</p>
+`;
+
+
+export default (o) => html`
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title" id="room-details-modal-label">${o.display_name}</h5>
+                ${modal_header_close_button}
+            </div>
+            <div class="modal-body">
+                <span class="modal-alert"></span>
+                <div class="room-info">
+                    <p class="room-info"><strong>${i18n_name}</strong>: ${o.name}</p>
+                    <p class="room-info"><strong>${i18n_address}</strong>: ${o.jid}</p>
+                    <p class="room-info"><strong>${i18n_desc}</strong>: ${o.config.description}</p>
+                    ${ (o.subject) ? subject(o) : '' }
+                    <p class="room-info"><strong>${i18n_online_users}</strong>: ${o.num_occupants}</p>
+                    <p class="room-info"><strong>${i18n_features}</strong>:
+                        <div class="chatroom-features">
+                        <ul class="features-list">
+                            ${ o.features.passwordprotected ? html`<li class="feature" ><span class="fa fa-lock"></span>${i18n_password_protected} - <em>${i18n_password_help}</em></li>` : '' }
+                            ${ o.features.unsecured ? html`<li class="feature" ><span class="fa fa-unlock"></span>${i18n_no_password_required} - <em>${i18n_no_pass_help}</em></li>` : '' }
+                            ${ o.features.hidden ? html`<li class="feature" ><span class="fa fa-eye-slash"></span>${i18n_hidden} - <em>${i18n_hidden_help}</em></li>` : '' }
+                            ${ o.features.public_room ? html`<li class="feature" ><span class="fa fa-eye"></span>${i18n_public} - <em>${o.__('This groupchat is publicly searchable') }</em></li>` : '' }
+                            ${ o.features.membersonly ? html`<li class="feature" ><span class="fa fa-address-book"></span>${i18n_members_only} - <em>${i18n_members_help}</em></li>` : '' }
+                            ${ o.features.open ? html`<li class="feature" ><span class="fa fa-globe"></span>${i18n_open} - <em>${i18n_open_help}</em></li>` : '' }
+                            ${ o.features.persistent ? html`<li class="feature" ><span class="fa fa-save"></span>${i18n_persistent} - <em>${i18n_persistent_help}</em></li>` : '' }
+                            ${ o.features.temporary ? html`<li class="feature" ><span class="fa fa-snowflake-o"></span>${i18n_temporary} - <em>${i18n_temporary_help}</em></li>` : '' }
+                            ${ o.features.nonanonymous ? html`<li class="feature" ><span class="fa fa-id-card"></span>${i18n_not_anonymous} - <em>${i18n_not_anonymous_help}</em></li>` : '' }
+                            ${ o.features.semianonymous ? html`<li class="feature" ><span class="fa fa-user-secret"></span>${i18n_semi_anon} - <em>${i18n_semi_anon_help}</em></li>` : '' }
+                            ${ o.features.moderated ? html`<li class="feature" ><span class="fa fa-gavel"></span>${i18n_moderated} - <em>${i18n_moderated_help}</em></li>` : '' }
+                            ${ o.features.unmoderated ? html`<li class="feature" ><span class="fa fa-info-circle"></span>${i18n_not_moderated} - <em>${i18n_not_moderated_help}</em></li>` : '' }
+                            ${ o.features.mam_enabled ? html`<li class="feature" ><span class="fa fa-database"></span>${i18n_archiving} - <em>${i18n_archiving_help}</em></li>` : '' }
+                        </ul>
+                        </div>
+                    </p>
+                </div>
+            </div>
+            <div class="modal-footer">${modal_close_button}</div>
+        </div>
+    </div>
+`;

+ 0 - 19
src/templates/chatroom_registration_modal.html

@@ -1,19 +0,0 @@
-<div class="modal" id="room-registration-modal" tabindex="-1" role="dialog" aria-labelledby="room-registration-modal-label" aria-hidden="true">
-    <div class="modal-dialog" role="document">
-        <div class="modal-content">
-            <div class="modal-header">
-                <h5 class="modal-title" id="room-registration-modal-label">{{{o.display_name}}}</h5>
-                <button type="button" class="close" data-dismiss="modal" aria-label="{{{o.label_close}}}"><span aria-hidden="true">×</span></button>
-            </div>
-            <div class="modal-body">
-                <form class="converse-form">
-                    {[ if (o.feedback.get('error')) { ]} <div class="alert alert-danger" role="alert">{{{o.feedback.get('error')}}}</div> {[ } ]}
-                    {[ if (!o.feedback.get('error')) { ]} <span class="spinner fa fa-spinner"></span> {[ } ]}
-                </form>
-            </div>
-            <div class="modal-footer">
-                <button type="button" class="btn btn-secondary" data-dismiss="modal">{{{o.__('Close')}}}</button>
-            </div>
-        </div>
-    </div>
-</div>

+ 0 - 21
src/templates/client_info_modal.html

@@ -1,21 +0,0 @@
-<!-- Change status Modal -->
-<div class="modal" id="modal-status-change" tabindex="-1" role="dialog" aria-labelledby="changeStatusModalLabel" aria-hidden="true">
-    <div class="modal-dialog" role="document">
-        <div class="modal-content">
-            <div class="modal-header">
-                <h5 class="modal-title" id="changeStatusModalLabel">{{{o.modal_title}}}</h5>
-                <button type="button" class="close" data-dismiss="modal" aria-label="{{{o.label_close}}}">
-                    <span aria-hidden="true">×</span>
-                </button>
-            </div>
-            <div class="modal-body">
-                <div class="container brand-heading-container">
-                    <h6 class="brand-heading">Converse</h6>
-                    <p class="brand-subtitle">{{{o.version_name}}}</p>
-                    <p class="brand-subtitle">{{o.first_subtitle}}</p>
-                    <p class="brand-subtitle">{{o.second_subtitle}}</p>
-                </div>
-            </div>
-        </div>
-    </div>
-</div>

+ 42 - 0
src/templates/client_info_modal.js

@@ -0,0 +1,42 @@
+import { __ } from '@converse/headless/i18n';
+import { html } from "lit-html";
+import { modal_header_close_button } from "./buttons"
+import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
+import xss from "xss/dist/xss";
+
+
+const modal_title = __('About');
+
+const first_subtitle = __(
+    '%1$s Open Source %2$s XMPP chat client brought to you by %3$s Opkode %2$s',
+    '<a target="_blank" rel="nofollow" href="https://conversejs.org">',
+    '</a>',
+    '<a target="_blank" rel="nofollow" href="https://opkode.com">'
+);
+
+const second_subtitle = __(
+    '%1$s Translate %2$s it into your own language',
+    '<a target="_blank" rel="nofollow" href="https://hosted.weblate.org/projects/conversejs/#languages">',
+    '</a>'
+);
+
+
+export default (o) => html`
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title" id="changeStatusModalLabel">${modal_title}</h5>
+                ${modal_header_close_button}
+            </div>
+            <div class="modal-body">
+                <span class="modal-alert"></span>
+                <div class="container brand-heading-container">
+                    <h6 class="brand-heading">Converse</h6>
+                    <p class="brand-subtitle">${o.version_name}</p>
+                    <p class="brand-subtitle">${unsafeHTML(xss.filterXSS(first_subtitle, {'whiteList': {'a': []}}))}</p>
+                    <p class="brand-subtitle">${unsafeHTML(xss.filterXSS(second_subtitle, {'whiteList': {'a': []}}))}</p>
+                </div>
+            </div>
+        </div>
+    </div>
+`;

+ 0 - 25
src/templates/list_chatrooms_modal.html

@@ -1,25 +0,0 @@
-<div class="modal" 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-content">
-            <div class="modal-header">
-                <h5 class="modal-title"
-                    id="list-chatrooms-modal-label">{{{o.heading_list_chatrooms}}}</h5>
-                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
-                    <span aria-hidden="true">×</span>
-                </button>
-            </div>
-            <div class="modal-body d-flex flex-column">
-                {[ if (o.show_form) { ]}
-                <form class="converse-form list-chatrooms">
-                    <div class="form-group">
-                        <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}}}"/>
-                    </div>
-                    <input type="submit" class="btn btn-primary" name="list" value="{{{o.label_query}}}"/>
-                </form>
-                {[ } ]}
-                <ul class="available-chatrooms list-group"></ul>
-            </div>
-        </div>
-    </div>
-</div>

+ 36 - 0
src/templates/list_chatrooms_modal.js

@@ -0,0 +1,36 @@
+import { html } from "lit-html";
+import { __ } from '@converse/headless/i18n';
+import { modal_close_button, modal_header_close_button } from "./buttons"
+
+const i18n_list_chatrooms = __('Query for Groupchats');
+const i18n_server_address = __('Server address');
+const i18n_query = __('Show groupchats');
+
+
+const form = (o) => html`
+    <form class="converse-form list-chatrooms">
+        <div class="form-group">
+            <label for="chatroom">${i18n_server_address}:</label>
+            <input type="text" value="${o.muc_domain}" required="required" name="server" class="form-control" placeholder="${o.server_placeholder}"/>
+        </div>
+        <input type="submit" class="btn btn-primary" name="list" value="${i18n_query}"/>
+    </form>
+`;
+
+
+export default (o) => html`
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title" id="list-chatrooms-modal-label">${i18n_list_chatrooms}</h5>
+                ${modal_header_close_button}
+            </div>
+            <div class="modal-body d-flex flex-column">
+                <span class="modal-alert"></span>
+                ${o.show_form ? form(o) : '' }
+                <ul class="available-chatrooms list-group"></ul>
+            </div>
+            <div class="modal-footer">${modal_close_button}</div>
+        </div>
+    </div>
+`;

+ 0 - 20
src/templates/message_versions_modal.html

@@ -1,20 +0,0 @@
-<div class="modal" id="message-versions-modal" tabindex="-1" role="dialog" aria-labelledby="message-versions-modal-label" aria-hidden="true">
-    <div class="modal-dialog" role="document">
-        <div class="modal-content">
-            <div class="modal-header">
-                <h4 class="modal-title" id="message-versions-modal-label">{{{o.__('Message versions')}}}</h4>
-                <button type="button" class="close" data-dismiss="modal" aria-label="{{{o.label_close}}}"><span aria-hidden="true">×</span></button>
-            </div>
-            <div class="modal-body">
-                <h4>Older versions</h4>
-                {[Object.keys(o.older_versions).forEach(function (k) { ]} <p class="older-msg"><time>{{{o.dayjs(k).format('MMM D, YYYY, HH:mm:ss')}}}</time>: {{{o.older_versions[k]}}}</p> {[ }); ]}
-                <hr/>
-                <h4>Current version</h4>
-                <p>{{{o.message}}}</p>
-            </div>
-            <div class="modal-footer">
-                <button type="button" class="btn btn-secondary" data-dismiss="modal">{{{o.__('Close')}}}</button>
-            </div>
-        </div>
-    </div>
-</div>

+ 27 - 0
src/templates/message_versions_modal.js

@@ -0,0 +1,27 @@
+import { html } from "lit-html";
+import { __ } from '@converse/headless/i18n';
+import dayjs from 'dayjs';
+import { modal_close_button, modal_header_close_button } from "./buttons"
+
+
+const i18n_message_versions = __('Message versions');
+
+
+export default (o) => html`
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h4 class="modal-title" id="message-versions-modal-label">${i18n_message_versions}</h4>
+                ${modal_header_close_button}
+            </div>
+            <div class="modal-body">
+                <h4>Older versions</h4>
+                ${Object.keys(o.older_versions).map(k => html`<p class="older-msg"><time>${dayjs(k).format('MMM D, YYYY, HH:mm:ss')}</time>: ${o.older_versions[k]}</p>`) }
+                <hr/>
+                <h4>Current version</h4>
+                <p>${o.message}</p>
+            </div>
+            <div class="modal-footer">${modal_close_button}</div>
+        </div>
+    </div>
+`;

+ 0 - 219
src/templates/moderator_tools_modal.html

@@ -1,219 +0,0 @@
-<div class="modal" id="converse-modtools-modal" tabindex="-1" role="dialog" aria-labelledby="converse-modtools-modal-label" aria-hidden="true">
-    <div class="modal-dialog" role="document">
-        <div class="modal-content">
-            <div class="modal-header">
-                <h5 class="modal-title" id="converse-modtools-modal-label">{{{o.__('Moderator Tools')}}}</h5>
-                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
-                    <span aria-hidden="true">×</span>
-                </button>
-            </div>
-            <div class="modal-body d-flex flex-column">
-                <ul class="nav nav-pills justify-content-center">
-                    <li role="presentation" class="nav-item">
-                        <a class="nav-link active" id="roles-tab" href="#roles-tabpanel" aria-controls="roles-tabpanel" role="tab" data-toggle="tab">Roles</a>
-                    </li>
-                    <li role="presentation" class="nav-item">
-                        <a class="nav-link" id="affiliations-tab" href="#affiliations-tabpanel" aria-controls="affiliations-tabpanel" role="tab" data-toggle="tab">Affiliations</a>
-                    </li>
-                </ul>
-
-                <div class="tab-content">
-                    <div class="tab-pane tab-pane--columns active" id="roles-tabpanel" role="tabpanel" aria-labelledby="roles-tab">
-                        <form class="converse-form query-role">
-                            <p class="helptext pb-3">
-{{{o.__("Roles are assigned to users to grant or deny them certain abilities in a multi-user chat. They're assigned either explicitly or implicitly as part of an affiliation. A role that's not due to an affiliation, is only valid for the duration of the user's session.")}}}
-                            </p>
-                            <div class="form-group">
-                                <label for="role">
-                                    <strong>{{{o.__('Role')}}}:</strong>
-                                </label>
-                                <div class="row">
-                                    <div class="col">
-                                        <select class="custom-select select-role" name="role">
-                                            {[ o.roles.forEach(function (role) { ]}
-                                                <option value="{{{role}}}" {[ if (role === o.role)  { ]} selected="selected" {[ } ]}
-                                                    {[ if (role === 'moderator')  { ]}
-                                                    title="{{{o.__("Moderators are privileged users who can change the roles of other users (except those with admin or owner affiliations.")}}}"
-                                                    {[ } ]}
-                                                    {[ if (role === 'participant')  { ]}
-                                                    title="{{{o.__("The default role, implies that you can read and write messages.")}}}"
-                                                    {[ } ]}
-                                                    {[ if (role === 'visitor')  { ]}
-                                                    title="{{{o.__("Visitors aren't allowed to write messages in a moderated multi-user chat.")}}}"
-                                                    {[ } ]}>{{{role}}}</option>
-                                            {[ }); ]}
-                                        </select>
-                                    </div>
-                                    <div class="col">
-                                        <input type="submit" class="btn btn-primary" name="users_with_role" value="{{{o.__('Show users')}}}"/>
-                                    </div>
-                                </div>
-                                <div class="row">
-                                    <div class="col pt-2">
-                                        {[ if (o.role === 'moderator')  { ]}
-                                        <p class="helptext pb-3">{{{o.__("Moderators are privileged users who can change the roles of other users (except those with admin or owner affiliations.")}}}</p>
-                                        {[ } ]}
-                                        {[ if (o.role === 'participant')  { ]}
-                                        <p class="helptext pb-3">{{{o.__("The default role, implies that you can read and write messages.")}}}</p>
-                                        {[ } ]}
-                                        {[ if (o.role === 'visitor')  { ]}
-                                        <p class="helptext pb-3">{{{o.__("Visitors aren't allowed to write messages in a moderated multi-user chat.")}}}</p>
-                                        {[ } ]}
-                                    </div>
-                                </div>
-                            </div>
-                        </form>
-                        <div class="scrollable-container">
-                        <ul class="list-group list-group--users">
-                            {[ if (o.loading_users_with_role)  { ]}
-                                <li class="list-group-item"> <span class="spinner fa fa-spinner centered"/> </li>
-                            {[ } ]}
-                            {[ if (o.users_with_role && o.users_with_role.length === 0) { ]}
-                                <li class="list-group-item">{{{o.__('No users with that role found.')}}}</li>
-                            {[ } ]}
-                            {[ (o.users_with_role || []).forEach(function (item) { ]}
-                                <li class="list-group-item">
-                                    <ul class="list-group">
-                                        <li class="list-group-item active">
-                                            <div><strong>JID:</strong> {{{item.jid}}}</div>
-                                        </li>
-                                        <li class="list-group-item">
-                                            <div><strong>Nickname:</strong> {{{item.nick}}}</div>
-                                        </li>
-                                        <li class="list-group-item">
-                                            <div><strong>Role:</strong> {{{item.role}}}<a href="#" data-form="role-form" class="toggle-form right fa fa-wrench"></a></div>
-                                            <form class="role-form hidden">
-                                                <div class="form-group">
-                                                    <input type="hidden" name="jid" value="{{{item.jid}}}"/>
-                                                    <input type="hidden" name="nick" value="{{{item.nick}}}"/>
-                                                    <div class="row">
-                                                        <div class="col">
-                                                            <label><strong>{{{o.__('New Role')}}}:</strong></label>
-                                                            <select class="custom-select select-role" name="role">
-                                                                {[ o.allowed_roles.forEach(function (role) { ]}
-                                                                    <option value="{{{role}}}" {[ if (role === item.role)  { ]} selected="selected" {[ } ]}>{{{role}}}</option>
-                                                                {[ }); ]}
-                                                            </select>
-                                                        </div>
-                                                        <div class="col">
-                                                            <label><strong>{{{o.__('Reason')}}}:</strong></label>
-                                                            <input class="form-control" type="text" name="reason"/>
-                                                        </div>
-                                                    </div>
-                                                </div>
-                                                <div class="form-group">
-                                                    <input type="submit" class="btn btn-primary" value="{{{o.__('Change role')}}}"/>
-                                                </div>
-                                            </form>
-                                        </li>
-                                    </ul>
-                                </li>
-                            {[ }); ]}
-                        </ul>
-                        </div>
-                    </div>
-
-
-                    <div class="tab-pane tab-pane--columns" id="affiliations-tabpanel" role="tabpanel" aria-labelledby="affiliations-tab">
-                        <form class="converse-form query-affiliation">
-                            <p class="helptext pb-3">
-{{{o.__("An affiliation is a long-lived entitlement which typically implies a certain role and which grants privileges and responsibilities. For example admins and owners automatically have the moderator role.")}}}
-                            </p>
-                            <div class="form-group">
-                                <label for="affiliation">
-                                    <strong>{{{o.__('Affiliation')}}}:</strong>
-                                </label>
-                                <div class="row">
-                                    <div class="col">
-                                        <select class="custom-select select-affiliation" name="affiliation">
-                                            {[ o.affiliations.forEach(function (aff) { ]}
-                                                <option value="{{{aff}}}" {[ if (aff === o.affiliation)  { ]} selected="selected" {[ } ]}
-                                                    {[ if (aff === 'owner')  { ]}
-                                                    title="{{{o.__("Owner is the highest affiliation. Owners can modify roles and affiliations of all other users.")}}}"
-                                                    {[ } ]}
-                                                    {[ if (aff === 'admin')  { ]}
-                                                    title="{{{o.__("Admin is the 2nd highest affiliation. Admins can modify roles and affiliations of all other users except owners.")}}}"
-                                                    {[ } ]}
-                                                    {[ if (aff === 'outcast')  { ]}
-                                                    title="{{{o.__("To ban a user, you give them the affiliation of \"outcast\".")}}}"
-                                                    {[ } ]}>{{{aff}}}</option>
-                                            {[ }); ]}
-                                        </select>
-                                    </div>
-                                    <div class="col">
-                                        <input type="submit" class="btn btn-primary" name="users_with_affiliation" value="{{{o.__('Show users')}}}"/>
-                                    </div>
-                                </div>
-                                <div class="row">
-                                    <div class="col pt-2">
-                                        {[ if (o.affiliation === 'owner')  { ]}
-                                        <p class="helptext pb-3">{{{o.__("Owner is the highest affiliation. Owners can modify roles and affiliations of all other users.")}}}</p>
-                                        {[ } ]}
-                                        {[ if (o.affiliation === 'admin')  { ]}
-                                        <p class="helptext pb-3">{{{o.__("Admin is the 2nd highest affiliation. Admins can modify roles and affiliations of all other users except owners.")}}}</p>
-                                        {[ } ]}
-                                        {[ if (o.affiliation === 'outcast')  { ]}
-                                        <p class="helptext pb-3">{{{o.__("To ban a user, you give them the affiliation of \"outcast\".")}}}</p>
-                                        {[ } ]}
-                                    </div>
-                                </div>
-                            </div>
-                        </form>
-                        <div class="scrollable-container">
-                        <ul class="list-group list-group--users">
-                            {[ if (o.loading_users_with_affiliation)  { ]}
-                                <li class="list-group-item"> <span class="spinner fa fa-spinner centered"/> </li>
-                            {[ } else { ]}
-                                {[ if (o.users_with_affiliation && o.users_with_affiliation.length === 0) { ]}
-                                    <li class="list-group-item">{{{o.__('No users with that affiliation found.')}}}</li>
-                                {[ } else if (o.users_with_affiliation instanceof Error) { ]}
-                                    <li class="list-group-item">{{{o.users_with_affiliation.message}}}</li>
-                                {[ } else { ]}
-                                    {[ (o.users_with_affiliation || []).forEach(function (item) { ]}
-                                        <li class="list-group-item">
-                                            <ul class="list-group">
-                                                <li class="list-group-item active">
-                                                    <div><strong>JID:</strong> {{{item.jid}}}</div>
-                                                </li>
-                                                <li class="list-group-item">
-                                                    <div><strong>Nickname:</strong> {{{item.nick}}}</div>
-                                                </li>
-                                                <li class="list-group-item">
-                                                    <div><strong>Affiliation:</strong> {{{item.affiliation}}} <a href="#" data-form="affiliation-form" class="toggle-form right fa fa-wrench"></a></div>
-                                                    <form class="affiliation-form hidden">
-                                                        <div class="form-group">
-                                                            <input type="hidden" name="jid" value="{{{item.jid}}}"/>
-                                                            <input type="hidden" name="nick" value="{{{item.nick}}}"/>
-                                                            <div class="row">
-                                                                <div class="col">
-                                                                    <label><strong>{{{o.__('New affiliation')}}}:</strong></label>
-                                                                    <select class="custom-select select-affiliation" name="affiliation">
-                                                                        {[ o.allowed_affiliations.forEach(function (aff) { ]}
-                                                                            <option value="{{{aff}}}" {[ if (aff === item.affiliation)  { ]} selected="selected" {[ } ]}>{{{aff}}}</option>
-                                                                        {[ }); ]}
-                                                                    </select>
-                                                                </div>
-                                                                <div class="col">
-                                                                    <label><strong>{{{o.__('Reason')}}}:</strong></label>
-                                                                    <input class="form-control" type="text" name="reason"/>
-                                                                </div>
-                                                            </div>
-                                                        </div>
-                                                        <div class="form-group">
-                                                            <input type="submit" class="btn btn-primary" name="change" value="{{{o.__('Change affiliation')}}}"/>
-                                                        </div>
-                                                    </form>
-                                                </li>
-                                            </ul>
-                                        </li>
-                                    {[ }); ]}
-                                {[ } ]}
-                            {[ } ]}
-                        </ul>
-                        </div>
-                    </div>
-                </div>
-            </div>
-        </div>
-    </div>
-</div>

+ 229 - 0
src/templates/moderator_tools_modal.js

@@ -0,0 +1,229 @@
+import { html } from "lit-html";
+import { __ } from '@converse/headless/i18n';
+import spinner from "./spinner.js";
+import { modal_header_close_button } from "./buttons"
+
+
+const i18n_affiliation = __('Affiliation');
+const i18n_change_affiliation = __('Change affiliation');
+const i18n_change_role = __('Change role');
+const i18n_moderator_tools = __('Moderator Tools');
+const i18n_new_affiliation = __('New affiliation');
+const i18n_new_role = __('New Role');
+const i18n_no_users_with_aff = __('No users with that affiliation found.')
+const i18n_no_users_with_role = __('No users with that role found.');
+const i18n_reason = __('Reason');
+const i18n_role = __('Role');
+const i18n_show_users = __('Show users');
+
+const i18n_helptext_role = __(
+    "Roles are assigned to users to grant or deny them certain abilities in a multi-user chat. "+
+    "They're assigned either explicitly or implicitly as part of an affiliation. "+
+    "A role that's not due to an affiliation, is only valid for the duration of the user's session."
+);
+
+const i18n_helptext_affiliation = __(
+    "An affiliation is a long-lived entitlement which typically implies a certain role and which "+
+    "grants privileges and responsibilities. For example admins and owners automatically have the "+
+    "moderator role."
+);
+
+
+function getRoleHelpText (role) {
+    if (role === 'moderator') {
+        return __("Moderators are privileged users who can change the roles of other users (except those with admin or owner affiliations.");
+    } else if (role === 'participant') {
+        return __("The default role, implies that you can read and write messages.");
+    } else if  (role == 'visitor') {
+        return __("Visitors aren't allowed to write messages in a moderated multi-user chat.");
+    }
+}
+
+function getAffiliationHelpText (aff) {
+    if (aff === 'owner') {
+        return __("Owner is the highest affiliation. Owners can modify roles and affiliations of all other users.");
+    } else if (aff === 'admin')  {
+        return __("Admin is the 2nd highest affiliation. Admins can modify roles and affiliations of all other users except owners.");
+    } else if (aff === 'outcast')  {
+        return __("To ban a user, you give them the affiliation of \"outcast\".");
+    }
+}
+
+
+const role_option = (o) => html`
+    <option value="${o.item || ''}"
+            ?selected=${o.item === o.role}
+            title="${getRoleHelpText(o.item)}">${o.item}</option>
+`;
+
+
+const affiliation_option = (o) => html`
+    <option value="${o.item || ''}"
+            ?selected=${o.item === o.affiliation}
+            title="${getAffiliationHelpText(o.item)}">${o.item}</option>
+`;
+
+
+const role_list_item = (o) => html`
+    <li class="list-group-item">
+        <ul class="list-group">
+            <li class="list-group-item active">
+                <div><strong>JID:</strong> ${o.item.jid}</div>
+            </li>
+            <li class="list-group-item">
+                <div><strong>Nickname:</strong> ${o.item.nick}</div>
+            </li>
+            <li class="list-group-item">
+                <div><strong>Role:</strong> ${o.item.role}<a href="#" data-form="role-form" class="toggle-form right fa fa-wrench"></a></div>
+                <form class="role-form hidden">
+                    <div class="form-group">
+                        <input type="hidden" name="jid" value="${o.item.jid}"/>
+                        <input type="hidden" name="nick" value="${o.item.nick}"/>
+                        <div class="row">
+                            <div class="col">
+                                <label><strong>${i18n_new_role}:</strong></label>
+                                <select class="custom-select select-role" name="role">
+                                    ${ o.allowed_roles.map(role => html`<option value="${role}" ?selected=${role === o.item.role}>${role}</option>`) }
+                                </select>
+                            </div>
+                            <div class="col">
+                                <label><strong>${i18n_reason}:</strong></label>
+                                <input class="form-control" type="text" name="reason"/>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <input type="submit" class="btn btn-primary" value="${i18n_change_role}"/>
+                    </div>
+                </form>
+            </li>
+        </ul>
+    </li>
+`;
+
+
+const affiliation_list_item = (o) => html`
+    <li class="list-group-item">
+        <ul class="list-group">
+            <li class="list-group-item active">
+                <div><strong>JID:</strong> ${o.item.jid}</div>
+            </li>
+            <li class="list-group-item">
+                <div><strong>Nickname:</strong> ${o.item.nick}</div>
+            </li>
+            <li class="list-group-item">
+                <div><strong>Affiliation:</strong> ${o.item.affiliation} <a href="#" data-form="affiliation-form" class="toggle-form right fa fa-wrench"></a></div>
+                <form class="affiliation-form hidden">
+                    <div class="form-group">
+                        <input type="hidden" name="jid" value="${o.item.jid}"/>
+                        <input type="hidden" name="nick" value="${o.item.nick}"/>
+                        <div class="row">
+                            <div class="col">
+                                <label><strong>${i18n_new_affiliation}:</strong></label>
+                                <select class="custom-select select-affiliation" name="affiliation">
+                                    ${ o.allowed_affiliations.map(aff => html`<option value="${aff}" ?selected=${aff === o.item.affiliation}>${aff}</option>`) }
+                                </select>
+                            </div>
+                            <div class="col">
+                                <label><strong>${i18n_reason}:</strong></label>
+                                <input class="form-control" type="text" name="reason"/>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="form-group">
+                        <input type="submit" class="btn btn-primary" name="change" value="${i18n_change_affiliation}"/>
+                    </div>
+                </form>
+            </li>
+        </ul>
+    </li>
+`;
+
+
+export default (o) => html`
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title" id="converse-modtools-modal-label">${i18n_moderator_tools}</h5>
+                ${modal_header_close_button}
+            </div>
+            <div class="modal-body d-flex flex-column">
+                <span class="modal-alert"></span>
+
+                <ul class="nav nav-pills justify-content-center">
+                    <li role="presentation" class="nav-item">
+                        <a class="nav-link active" id="roles-tab" href="#roles-tabpanel" aria-controls="roles-tabpanel" role="tab" data-toggle="tab">Roles</a>
+                    </li>
+                    <li role="presentation" class="nav-item">
+                        <a class="nav-link" id="affiliations-tab" href="#affiliations-tabpanel" aria-controls="affiliations-tabpanel" role="tab" data-toggle="tab">Affiliations</a>
+                    </li>
+                </ul>
+
+                <div class="tab-content">
+                    <div class="tab-pane tab-pane--columns active" id="roles-tabpanel" role="tabpanel" aria-labelledby="roles-tab">
+                        <form class="converse-form query-role">
+                            <p class="helptext pb-3">${i18n_helptext_role}</p>
+                            <div class="form-group">
+                                <label for="role"><strong>${i18n_role}:</strong></label>
+                                <div class="row">
+                                    <div class="col">
+                                        <select class="custom-select select-role" name="role">
+                                            ${o.roles.map(item => role_option(Object.assign({item}, o)))}
+                                        </select>
+                                    </div>
+                                    <div class="col">
+                                        <input type="submit" class="btn btn-primary" name="users_with_role" value="${i18n_show_users}"/>
+                                    </div>
+                                </div>
+                                <div class="row">
+                                    <div class="col pt-2"><p class="helptext pb-3">${getRoleHelpText(o.role)}</p></div>
+                                </div>
+                            </div>
+                        </form>
+                        <div class="scrollable-container">
+                            <ul class="list-group list-group--users">
+                                ${ o.loading_users_with_role ? html`<li class="list-group-item"> ${spinner()} </li>` : '' }
+                                ${ (o.users_with_role && o.users_with_role.length === 0) ? html`<li class="list-group-item">${i18n_no_users_with_role}</li>` : '' }
+                                ${ (o.users_with_role || []).map(item => role_list_item(Object.assign({item}, o))) }
+                            </ul>
+                        </div>
+                    </div>
+
+
+                    <div class="tab-pane tab-pane--columns" id="affiliations-tabpanel" role="tabpanel" aria-labelledby="affiliations-tab">
+                        <form class="converse-form query-affiliation">
+                            <p class="helptext pb-3">${i18n_helptext_affiliation}</p>
+                            <div class="form-group">
+                                <label for="affiliation">
+                                    <strong>${i18n_affiliation}:</strong>
+                                </label>
+                                <div class="row">
+                                    <div class="col">
+                                        <select class="custom-select select-affiliation" name="affiliation">
+                                            ${o.affiliations.map(item => affiliation_option(Object.assign({item}, o)))}
+                                        </select>
+                                    </div>
+                                    <div class="col">
+                                        <input type="submit" class="btn btn-primary" name="users_with_affiliation" value="${i18n_show_users}"/>
+                                    </div>
+                                </div>
+                                <div class="row">
+                                    <div class="col pt-2"><p class="helptext pb-3">${getAffiliationHelpText(o.affiliation)}</p></div>
+                                </div>
+                            </div>
+                        </form>
+                        <div class="scrollable-container">
+                            <ul class="list-group list-group--users">
+                                ${ (o.loading_users_with_affiliation) ? html`<li class="list-group-item"> ${spinner()} </li>` : '' }
+                                ${ (Array.isArray(o.users_with_affiliation) && o.users_with_affiliation.length === 0) ? html`<li class="list-group-item">${i18n_no_users_with_aff}</li>` : '' }
+                                ${ (o.users_with_affiliation instanceof Error) ?
+                                        html`<li class="list-group-item">${o.users_with_affiliation.message}</li>` :
+                                        (o.users_with_affiliation || []).map(item => affiliation_list_item(Object.assign({item}, o))) }
+                            </ul>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+`;

+ 32 - 0
src/templates/profile.js

@@ -0,0 +1,32 @@
+import { html } from "lit-html";
+import { __ } from '@converse/headless/i18n';
+
+
+const i18n_logout = __('Log out');
+const i18n_change_status = __('Click to change your chat status');
+const i18n_details = __('Show details about this chat client');
+
+
+export default (o) => html`
+    <div class="userinfo controlbox-padded">
+        <div class="controlbox-section profile d-flex">
+            <a class="show-profile" href="#">
+                <canvas class="avatar align-self-center" height="40" width="40"></canvas>
+            </a>
+            <span class="username w-100 align-self-center">${o.fullname}</span>
+            ${o._converse.show_client_info && html`<a class="controlbox-heading__btn show-client-info fa fa-info-circle align-self-center" title="${i18n_details}"></a>`}
+            ${o._converse.allow_logout && html`<a class="controlbox-heading__btn logout fa fa-sign-out-alt align-self-center" title="${i18n_logout}"></a>`}
+        </div>
+        <div class="d-flex xmpp-status">
+            <a class="change-status" title="${i18n_change_status}" data-toggle="modal" data-target="#changeStatusModal">
+                <span class="${o.chat_status} w-100 align-self-center" data-value="${o.chat_status}">
+                    <span class="
+                        ${o.chat_status === 'online' && 'fa fa-circle chat-status chat-status--online'}
+                        ${o.chat_status === 'dnd' && 'fa fa-minus-circle chat-status chat-status--busy'}
+                        ${o.chat_status === 'away' && 'fa fa-circle chat-status chat-status--away'}
+                        ${o.chat_status === 'xa' && 'far fa-circle chat-status chat-status--xa '}
+                        ${o.chat_status === 'offline' && 'fa fa-circle chat-status chat-status--offline'}"></span> ${o.status_message}</span>
+            </a>
+        </div>
+    </div>
+`;

+ 0 - 125
src/templates/profile_modal.html

@@ -1,125 +0,0 @@
-<div class="modal" id="user-profile-modal" tabindex="-1" role="dialog" aria-labelledby="user-profile-modal-label" aria-hidden="true">
-    <div class="modal-dialog" role="document">
-        <div class="modal-content">
-            <div class="modal-header">
-                <h5 class="modal-title" id="user-profile-modal-label">{{{o.heading_profile}}}</h5>
-                <button type="button" class="close" data-dismiss="modal" aria-label="{{{o.label_close}}}"><span aria-hidden="true">×</span></button>
-            </div>
-            <div class="modal-body">
-                {[ if (o._converse.pluggable.plugins['converse-omemo'].enabled(o._converse)) { ]}
-                <ul class="nav nav-pills justify-content-center">
-                    <li role="presentation" class="nav-item">
-                        <a class="nav-link active" id="profile-tab" href="#profile-tabpanel" aria-controls="profile-tabpanel" role="tab" data-toggle="tab">Profile</a>
-                    </li>
-                    <li role="presentation" class="nav-item">
-                        <a class="nav-link" id="omemo-tab" href="#omemo-tabpanel" aria-controls="omemo-tabpanel" role="tab" data-toggle="tab">OMEMO</a>
-                    </li>
-                </ul>
-                {[ } ]}
-                <div class="tab-content">
-                    <div class="tab-pane active" id="profile-tabpanel" role="tabpanel" aria-labelledby="profile-tab">
-                        <form class="converse-form converse-form--modal profile-form" action="#">
-                            <div class="row">
-                                <div class="col-auto">
-                                    <a class="change-avatar" href="#">
-                                        {[ if (o.image) { ]}
-                                            <img alt="{{{o.alt_avatar}}}" class="img-thumbnail avatar align-self-center" height="100px" width="100px" src="data:{{{o.image_type}}};base64,{{{o.image}}}"/>
-                                        {[ } ]}
-                                        {[ if (!o.image) { ]}
-                                            <canvas class="avatar" height="100px" width="100px"></canvas>
-                                        {[ } ]}
-                                    </a>
-                                    <input class="hidden" name="image" type="file"/>
-                                </div>
-                                <div class="col">
-                                    <div class="form-group">
-                                        <label class="col-form-label">{{{o.label_jid}}}:</label>
-                                        <div>{{{o.jid}}}</div>
-                                    </div>
-                                </div>
-                            </div>
-                            <div class="form-group">
-                                <label for="vcard-fullname" class="col-form-label">{{{o.label_fullname}}}:</label>
-                                <input id="vcard-fullname" type="text" class="form-control" name="fn" value="{{{o.fullname}}}"/>
-                            </div>
-                            <div class="form-group">
-                                <label for="vcard-nickname" class="col-form-label">{{{o.label_nickname}}}:</label>
-                                <input id="vcard-nickname" type="text" class="form-control" name="nickname" value="{{{o.nickname}}}"/>
-                            </div>
-                            <div class="form-group">
-                                <label for="vcard-url" class="col-form-label">{{{o.label_url}}}:</label>
-                                <input id="vcard-url" type="url" class="form-control" name="url" value="{{{o.url}}}"/>
-                            </div>
-                            <div class="form-group">
-                                <label for="vcard-email" class="col-form-label">{{{o.label_email}}}:</label>
-                                <input id="vcard-email" type="email" class="form-control" name="email" value="{{{o.email}}}"/>
-                            </div>
-                            <div class="form-group">
-                                <label for="vcard-role" class="col-form-label">{{{o.label_role}}}:</label>
-                                <input id="vcard-role" type="text" class="form-control" name="role" value="{{{o.role}}}" aria-describedby="vcard-role-help"/>
-                                <small id="vcard-role-help" class="form-text text-muted">{{{o.label_role_help}}}</small>
-                            </div>
-                            <hr/>
-                            <div class="form-group">
-                                <button type="submit" class="save-form btn btn-primary">{{{o.__('Save and close')}}}</button>
-                            </div>
-                        </form>
-                    </div>
-                    {[ if (o._converse.pluggable.plugins['converse-omemo'].enabled(o._converse)) { ]}
-                        <div class="tab-pane" id="omemo-tabpanel" role="tabpanel" aria-labelledby="omemo-tab">
-                            <form class="converse-form fingerprint-removal">
-                                <ul class="list-group fingerprints">
-                                    <li class="list-group-item active">{{{o.__("This device's OMEMO fingerprint")}}}</li>
-                                    <li class="list-group-item">
-                                    {[ if (o.view.current_device && o.view.current_device.get('bundle') && o.view.current_device.get('bundle').fingerprint) { ]}
-                                        <span class="fingerprint">{{{o.utils.formatFingerprint(o.view.current_device.get('bundle').fingerprint)}}}</span>
-                                    {[ } else {]}
-                                        <span class="spinner fa fa-spinner centered"/>
-                                    {[ } ]}
-                                    </li>
-                                </ul>
-                                <div class="form-group">
-                                    <button type="button" class="generate-bundle btn btn-danger">{{{o.__('Generate new keys and fingerprint')}}}</button>
-                                </div>
-
-                                {[ if (o.view.other_devices.length) { ]}
-                                    <ul class="list-group fingerprints">
-                                        <li class="list-group-item nopadding active">
-                                            <label>
-                                            <input type="checkbox" class="select-all" title="{{{o.__('Select all')}}}"
-                                                   aria-label="{{{o.__('Checkbox to select fingerprints of all other OMEMO devices')}}}"/>
-                                            {{{o.__('Other OMEMO-enabled devices')}}}
-                                            </label>
-                                        </li>
-                                        {[ o.view.other_devices.forEach(function (device) { ]}
-                                            {[ if (device.get('bundle') && device.get('bundle').fingerprint) { ]}
-                                            <li class="fingerprint-removal-item list-group-item nopadding">
-                                                <label>
-                                                <input type="checkbox" value="{{{device.get('id')}}}"
-                                                       aria-label="{{{o.__('Checkbox for selecting the following fingerprint')}}}"/>
-                                                <span class="fingerprint">{{{o.utils.formatFingerprint(device.get('bundle').fingerprint)}}}</span>
-                                                </label>
-                                            </li>
-                                            {[ } else {]}
-                                            <li class="fingerprint-removal-item list-group-item nopadding">
-                                                <label>
-                                                <input type="checkbox" value="{{{device.get('id')}}}"
-                                                       aria-label="{{{o.__('Checkbox for selecting the following fingerprint')}}}"/>
-                                                <span>{{{o.__('Device without a fingerprint')}}}</span>
-                                                </label>
-                                            </li>
-                                            {[ } ]}
-                                        {[ }); ]}
-                                    </ul>
-                                    <div class="form-group">
-                                        <button type="submit" class="save-form btn btn-primary">{{{o.__('Remove checked devices and close')}}}</button>
-                                    </div>
-                                {[ } ]}
-                            </form>
-                        </div>
-                    {[ } ]}
-                </div>
-            </div>
-        </div>
-    </div>
-</div>

+ 165 - 0
src/templates/profile_modal.js

@@ -0,0 +1,165 @@
+import { html } from "lit-html";
+import { __ } from '@converse/headless/i18n';
+import avatar from "./avatar.js";
+import spinner from "./spinner.js";
+import { modal_close_button, modal_header_close_button } from "./buttons"
+
+
+const alt_avatar = __('Your avatar image');
+const heading_profile = __('Your Profile');
+const i18n_close = __('Close');
+const i18n_fingerprint_checkbox_label = __('Checkbox for selecting the following fingerprint');
+const i18n_device_without_fingerprint = __('Device without a fingerprint');
+const i18n_email = __('Email');
+const i18n_fingerprint = __("This device's OMEMO fingerprint");
+const i18n_fullname = __('Full Name');
+const i18n_generate = __('Generate new keys and fingerprint');
+const i18n_jid = __('XMPP Address (JID)');
+const i18n_nickname = __('Nickname');
+const i18n_other_devices = __('Other OMEMO-enabled devices');
+const i18n_other_devices_label = __('Checkbox to select fingerprints of all other OMEMO devices');
+const i18n_remove_devices = __('Remove checked devices and close');
+const i18n_role = __('Role');
+const i18n_save = __('Save and close');
+const i18n_select_all = __('Select all');
+const i18n_role_help = __(
+    'Use commas to separate multiple roles. '+
+    'Your roles are shown next to your name on your chat messages.');
+const i18n_url = __('URL');
+const i18n_omemo = __('OMEMO');
+const i18n_profile = __('Profile');
+
+const navigation =  html`
+    <ul class="nav nav-pills justify-content-center">
+        <li role="presentation" class="nav-item">
+            <a class="nav-link active" id="profile-tab" href="#profile-tabpanel" aria-controls="profile-tabpanel" role="tab" data-toggle="tab">${i18n_profile}</a>
+        </li>
+        <li role="presentation" class="nav-item">
+            <a class="nav-link" id="omemo-tab" href="#omemo-tabpanel" aria-controls="omemo-tabpanel" role="tab" data-toggle="tab">${i18n_omemo}</a>
+        </li>
+    </ul>`;
+
+
+const fingerprint = (o) => html`
+    <span class="fingerprint">${o.utils.formatFingerprint(o.view.current_device.get('bundle').fingerprint)}</span>`;
+
+
+const device_with_fingerprint = (o) => html`
+    <li class="fingerprint-removal-item list-group-item nopadding">
+        <label>
+        <input type="checkbox" value="${o.device.get('id')}"
+            aria-label="${i18n_fingerprint_checkbox_label}"/>
+        <span class="fingerprint">${o.utils.formatFingerprint(o.device.get('bundle').fingerprint)}</span>
+        </label>
+    </li>
+`;
+
+
+const device_without_fingerprint = () => html`
+    <li class="fingerprint-removal-item list-group-item nopadding">
+        <label>
+        <input type="checkbox" value="${o.device.get('id')}"
+            aria-label="${i18n_fingerprint_checkbox_label}"/>
+        <span>${i18n_device_without_fingerprint}</span>
+        </label>
+    </li>
+`;
+
+
+const device_item = (o) => html`
+    ${(o.device.get('bundle') && o.device.get('bundle').fingerprint) ? device_with_fingerprint(o) : device_without_fingerprint(o) }
+`;
+
+
+const device_list = (o) => html`
+    <ul class="list-group fingerprints">
+        <li class="list-group-item nopadding active">
+            <label>
+                <input type="checkbox" class="select-all" title="${i18n_select_all}" aria-label="${i18n_other_devices_label}"/>
+                ${i18n_other_devices}
+            </label>
+        </li>
+        ${ o.view.other_devices.map(device => device_item(Object.assign({device}, o))) }
+    </ul>
+    <div class="form-group"><button type="submit" class="save-form btn btn-primary">${i18n_remove_devices}</button></div>
+`;
+
+
+const omemo_page = (o) => html`
+    <div class="tab-pane" id="omemo-tabpanel" role="tabpanel" aria-labelledby="omemo-tab">
+        <form class="converse-form fingerprint-removal">
+            <ul class="list-group fingerprints">
+                <li class="list-group-item active">${i18n_fingerprint}</li>
+                <li class="list-group-item">
+                    ${ (o.view.current_device && o.view.current_device.get('bundle') && o.view.current_device.get('bundle').fingerprint) ? fingerprint(o) : spinner() }
+                </li>
+            </ul>
+            <div class="form-group">
+                <button type="button" class="generate-bundle btn btn-danger">${i18n_generate}</button>
+            </div>
+            ${ o.view.other_devices.length ? device_list(o) : '' }
+        </form>
+    </div>`;
+
+
+export default (o) => html`
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title" id="user-profile-modal-label">${heading_profile}</h5>
+                ${modal_header_close_button}
+            </div>
+            <div class="modal-body">
+                <span class="modal-alert"></span>
+                ${o._converse.pluggable.plugins['converse-omemo'].enabled(o._converse) && navigation}
+                <div class="tab-content">
+                    <div class="tab-pane active" id="profile-tabpanel" role="tabpanel" aria-labelledby="profile-tab">
+                        <form class="converse-form converse-form--modal profile-form" action="#">
+                            <div class="row">
+                                <div class="col-auto">
+                                    <a class="change-avatar" href="#">
+                                        ${o.image ? avatar(Object.assign({'alt_text': alt_avatar}, o)) : '<canvas class="avatar" height="100px" width="100px"></canvas>'}
+                                    </a>
+                                    <input class="hidden" name="image" type="file"/>
+                                </div>
+                                <div class="col">
+                                    <div class="form-group">
+                                        <label class="col-form-label">${i18n_jid}:</label>
+                                        <div>${o.jid}</div>
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="form-group">
+                                <label for="vcard-fullname" class="col-form-label">${i18n_fullname}:</label>
+                                <input id="vcard-fullname" type="text" class="form-control" name="fn" value="${o.fullname || ''}"/>
+                            </div>
+                            <div class="form-group">
+                                <label for="vcard-nickname" class="col-form-label">${i18n_nickname}:</label>
+                                <input id="vcard-nickname" type="text" class="form-control" name="nickname" value="${o.nickname || ''}"/>
+                            </div>
+                            <div class="form-group">
+                                <label for="vcard-url" class="col-form-label">${i18n_url}:</label>
+                                <input id="vcard-url" type="url" class="form-control" name="url" value="${o.url || ''}"/>
+                            </div>
+                            <div class="form-group">
+                                <label for="vcard-email" class="col-form-label">${i18n_email}:</label>
+                                <input id="vcard-email" type="email" class="form-control" name="email" value="${o.email || ''}"/>
+                            </div>
+                            <div class="form-group">
+                                <label for="vcard-role" class="col-form-label">${i18n_role}:</label>
+                                <input id="vcard-role" type="text" class="form-control" name="role" value="${o.role || ''}" aria-describedby="vcard-role-help"/>
+                                <small id="vcard-role-help" class="form-text text-muted">${i18n_role_help}</small>
+                            </div>
+                            <hr/>
+                            <div class="form-group">
+                                <button type="submit" class="save-form btn btn-primary">${i18n_save}</button>
+                            </div>
+                        </form>
+                    </div>
+                    ${ _converse.pluggable.plugins['converse-omemo'].enabled(_converse) && omemo_page(o) }
+                </div>
+            </div>
+            <div class="modal-footer">${modal_close_button}</div>
+        </div>
+    </div>
+`;

+ 0 - 25
src/templates/profile_view.html

@@ -1,25 +0,0 @@
-<div class="userinfo controlbox-padded">
-<div class="controlbox-section profile d-flex">
-    <a class="show-profile" href="#">
-        <canvas class="avatar align-self-center" height="40" width="40"></canvas>
-    </a>
-    <span class="username w-100 align-self-center">{{{o.fullname}}}</span>
-    {[ if (o._converse.show_client_info) { ]}
-        <a class="controlbox-heading__btn show-client-info fa fa-info-circle align-self-center" title="{{{o.info_details}}}"></a>
-    {[ } ]}
-    {[ if (o._converse.allow_logout) { ]}
-        <a class="controlbox-heading__btn logout fa fa-sign-out-alt align-self-center" title="{{{o.title_log_out}}}"></a>
-    {[ } ]}
-</div>
-<div class="d-flex xmpp-status">
-    <a class="change-status" title="{{{o.title_change_status}}}" data-toggle="modal" data-target="#changeStatusModal">
-        <span class="{{{o.chat_status}}} w-100 align-self-center" data-value="{{{o.chat_status}}}">
-            <span class="
-                {[ if (o.chat_status === 'online') { ]} fa fa-circle chat-status chat-status--online{[ } ]}
-                {[ if (o.chat_status === 'dnd') { ]} fa fa-minus-circle chat-status chat-status--busy {[ } ]}
-                {[ if (o.chat_status === 'away') { ]} fa fa-circle chat-status chat-status--away{[ } ]}
-                {[ if (o.chat_status === 'xa') { ]} far fa-circle chat-status chat-status--xa {[ } ]}
-                {[ if (o.chat_status === 'offline') { ]} fa fa-circle chat-status chat-status--offline{[ } ]}"></span> {{{o.status_message}}}</span>
-    </a>
-</div>
-</div>

+ 0 - 30
src/templates/prompt.html

@@ -1,30 +0,0 @@
-<div class="modal" tabindex="-1" role="dialog">
-  <div class="modal-dialog" role="document">
-    <div class="modal-content">
-      <div class="modal-header {{{o.level}}}">
-        <h5 class="modal-title">{{{o.title}}}</h5>
-        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
-          <span aria-hidden="true">×</span>
-        </button>
-      </div>
-      <div class="modal-body">
-          <form class="converse-form converse-form--modal confirm" action="#">
-            <div class="form-group">
-                {[o.messages.forEach(function (message) { ]}
-                    <p>{{{message}}}</p>
-                {[ }) ]}
-            </div>
-            {[ if (o.type === 'prompt') { ]}
-              <div class="form-group">
-                  <input type="text" name="reason" class="form-control" placeholder="{{{o.placeholder}}}"/>
-              </div>
-            {[ } ]}
-            <div class="form-group">
-                <button type="submit" class="btn btn-primary">{{{o.__('OK')}}}</button>
-                <input type="button" class="btn btn-secondary" data-dismiss="modal" value="{{{o.__('Cancel')}}}"/>
-            </div>
-        </form>
-      </div>
-    </div>
-  </div>
-</div>

+ 37 - 0
src/templates/prompt.js

@@ -0,0 +1,37 @@
+import { html } from "lit-html";
+import { __ } from '@converse/headless/i18n';
+
+
+const i18n_cancel = __('Cancel');
+const i18n_ok = __('OK');
+
+
+export default (o) => html`
+    <div class="modal-dialog" role="document">
+      <div class="modal-content">
+        <div class="modal-header ${o.level}">
+          <h5 class="modal-title">${o.title}</h5>
+          <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+            <span aria-hidden="true">×</span>
+          </button>
+        </div>
+        <div class="modal-body">
+            <span class="modal-alert"></span>
+            <form class="converse-form converse-form--modal confirm" action="#">
+              <div class="form-group">
+                  ${ o.messages.map(message => html`<p>${message}</p>`) }
+              </div>
+              {[ if (o.type === 'prompt') { ]}
+                <div class="form-group">
+                    <input type="text" name="reason" class="form-control" placeholder="${o.placeholder}"/>
+                </div>
+              {[ } ]}
+              <div class="form-group">
+                  <button type="submit" class="btn btn-primary">${i18n_ok}</button>
+                  <input type="button" class="btn btn-secondary" data-dismiss="modal" value="${i18n_cancel}"/>
+              </div>
+          </form>
+        </div>
+      </div>
+    </div>
+`;

+ 3 - 0
src/templates/spinner.js

@@ -0,0 +1,3 @@
+import { html } from "lit-html";
+
+export default () => html`<span class="spinner fa fa-spinner centered"/>`

+ 0 - 71
src/templates/user_details_modal.html

@@ -1,71 +0,0 @@
-<div class="modal" id="user-details-modal" tabindex="-1" role="dialog" aria-labelledby="user-details-modal-label" aria-hidden="true">
-    <div class="modal-dialog" role="document">
-        <div class="modal-content">
-            <div class="modal-header">
-                <h5 class="modal-title" id="user-details-modal-label">{{{o.display_name}}}</h5>
-                <button type="button" class="close" data-dismiss="modal" aria-label="{{{o.__('Close')}}}"><span aria-hidden="true">×</span></button>
-            </div>
-            <div class="modal-body">
-                {[ if (o.image) { ]}
-                <img alt="{{{o.__('The User\'s Profile Image')}}}"
-                    class="img-thumbnail avatar align-self-center mb-3"
-                    height="100" width="100" src="data:{{{o.image_type}}};base64,{{{o.image}}}"/>
-                {[ } ]}
-                {[ if (o.fullname) { ]}
-                <p><label>{{{o.__('Full Name:')}}}</label> {{{o.fullname}}}</p>
-                {[ } ]}
-                <p><label>{{{o.__('XMPP Address:')}}}</label> <a href="xmpp:{{{o.jid}}}">{{{o.jid}}}</a></p>
-                {[ if (o.nickname) { ]}
-                <p><label>{{{o.__('Nickname:')}}}</label> {{{o.nickname}}}</p>
-                {[ } ]}
-                {[ if (o.url) { ]}
-                <p><label>{{{o.__('URL:')}}}</label> <a target="_blank" rel="noopener" href="{{{o.url}}}">{{{o.url}}}</a></p>
-                {[ } ]}
-                {[ if (o.email) { ]}
-                <p><label>{{{o.__('Email:')}}}</label> <a href="mailto:{{{o.email}}}">{{{o.email}}}</a></p>
-                {[ } ]}
-                {[ if (o.role) { ]}
-                <p><label>{{{o.__('Role:')}}}</label> {{{o.role}}}</p>
-                {[ } ]}
-
-                {[ if (o._converse.pluggable.plugins['converse-omemo'].enabled(o._converse)) { ]}
-                    <hr/>
-                    <ul class="list-group fingerprints">
-                        <li class="list-group-item active">{{{o.__('OMEMO Fingerprints')}}}</li>
-                        {[ if (!o.view.devicelist.devices) { ]}
-                            <li class="list-group-item"><span class="spinner fa fa-spinner centered"/></li>
-                        {[ } ]}
-                        {[ if (o.view.devicelist.devices) { ]}
-                            {[ o.view.devicelist.devices.each(function (device) { ]}
-                                {[ if (device.get('bundle') && device.get('bundle').fingerprint) { ]}
-                                <li class="list-group-item">
-                                    <form class="fingerprint-trust">
-                                    <div class="btn-group btn-group-toggle">
-                                        <label class="btn btn--small {[ if (device.get('trusted') !== -1) { ]} btn-primary active {[ } else { ]}  btn-secondary {[ } ]}">
-                                            <input type="radio" name="{{{device.get('id')}}}" value="1"
-                                                {[ if (device.get('trusted') !== -1) { ]} checked="checked" {[ } ]}/>{{{o.__('Trusted')}}}
-                                        </label>
-                                        <label class="btn btn--small {[ if (device.get('trusted') === -1) { ]} btn-primary active {[ } else { ]} btn-secondary {[ } ]}">
-                                            <input type="radio" name="{{{device.get('id')}}}" value="-1"
-                                                {[ if (device.get('trusted') === -1) { ]} checked="checked" {[ } ]}/>{{{o.__('Untrusted')}}}
-                                        </label>
-                                    </div>
-                                    <span class="fingerprint">{{{o.utils.formatFingerprint(device.get('bundle').fingerprint)}}}</span>
-                                    </form>
-                                </li>
-                                {[ } ]}
-                            {[ }); ]}
-                        {[ } ]}
-                    </ul>
-                {[ } ]}
-            </div>
-            <div class="modal-footer">
-                <button type="button" class="btn btn-warning" data-dismiss="modal">{{{o.__('Close')}}}</button>
-                <button type="button" class="btn btn-info refresh-contact"><i class="fa fa-refresh"> </i>{{{o.__('Refresh')}}}</button>
-                {[ if (o.allow_contact_removal && o.is_roster_contact) { ]}
-                    <button type="button" class="btn btn-danger remove-contact"><i class="far fa-trash-alt"> </i>{{{o.__('Remove as contact')}}}</button>
-                {[ } ]}
-            </div>
-        </div>
-    </div>
-</div>

+ 93 - 0
src/templates/user_details_modal.js

@@ -0,0 +1,93 @@
+import { __ } from '@converse/headless/i18n';
+import { html } from "lit-html";
+import avatar from "./avatar.js";
+import spinner from "./spinner.js";
+import { modal_close_button, modal_header_close_button } from "./buttons"
+
+
+const i18n_address = __('XMPP Address');
+const i18n_email = __('Email');
+const i18n_fingerprints = __('OMEMO Fingerprints');
+const i18n_full_name = __('Full Name');
+const i18n_nickname = __('Nickname');
+const i18n_profile = __('The User\'s Profile Image');
+const i18n_refresh = __('Refresh');
+const i18n_role = __('Role');
+const i18n_url = __('URL');
+const i18n_remove_contact = __('Remove as contact');
+const i18n_trusted = __('Trusted');
+const i18n_untrusted = __('Untrusted');
+const i18n_no_devices = __("No OMEMO-enabled devices found");
+
+const avatar_data = {
+    'alt_text': i18n_profile,
+    'extra_classes': 'mb-3'
+}
+
+
+const device_fingerprint = (o) => {
+    if (o.device.get('bundle') && o.device.get('bundle').fingerprint) {
+        return html`
+            <li class="list-group-item">
+                <form class="fingerprint-trust">
+                <div class="btn-group btn-group-toggle">
+                    <label class="btn btn--small ${(o.device.get('trusted') !== -1) ? 'btn-primary active' : 'btn-secondary'}">
+                        <input type="radio" name="${o.device.get('id')}" value="1" ?checked=${o.device.get('trusted') !== -1}>${i18n_trusted}
+                    </label>
+                    <label class="btn btn--small ${(o.device.get('trusted') !== -1) ? 'btn-primary active' : 'btn-secondary'}">
+                        <input type="radio" name="${o.device.get('id')}" value="-1" ?checked=${o.device.get('trusted') === -1}>${i18n_untrusted}
+                    </label>
+                </div>
+                <span class="fingerprint">${o.utils.formatFingerprint(o.device.get('bundle').fingerprint)}</span>
+                </form>
+            </li>
+        `;
+    } else {
+        return ''
+    }
+}
+
+
+const fingerprints = (o) => {
+    const devices = o.view.devicelist.devices;
+    return html`
+        <hr/>
+        <ul class="list-group fingerprints">
+            <li class="list-group-item active">${i18n_fingerprints}</li>
+            ${ devices.length ?
+                    devices.map(device => device_fingerprint(Object.assign({device}, o))) :
+                    html`<li class="list-group-item"> ${i18n_no_devices} </li>` }
+        </ul>
+    `;
+}
+
+const remove_button = html`<button type="button" class="btn btn-danger remove-contact"><i class="far fa-trash-alt"> </i>${i18n_remove_contact}</button>`;
+
+
+export default (o) => html`
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title" id="user-details-modal-label">${o.display_name}</h5>
+                ${modal_header_close_button}
+            </div>
+            <div class="modal-body">
+                ${ o.image ? avatar(Object.assign(avatar_data, o)) : '' }
+                ${ o.fullname ? html`<p><label>${i18n_full_name}:</label> ${o.fullname}</p>` : '' }
+                <p><label>${i18n_address}:</label> <a href="xmpp:${o.jid}">${o.jid}</a></p>
+                ${ o.nickname ? html`<p><label>${i18n_nickname}:</label> ${o.nickname}</p>` : '' }
+                ${ o.url ? html`<p><label>${i18n_url}:</label> <a target="_blank" rel="noopener" href="${o.url}">${o.url}</a></p>` : '' }
+                ${ o.email ? html`<p><label>${i18n_email}:</label> <a href="mailto:${o.email}">${o.email}</a></p>` : '' }
+                ${ o.role ? html`<p><label>${i18n_role}:</label> ${o.role}</p>` : '' }
+
+                ${ (o._converse.pluggable.plugins['converse-omemo'].enabled(o._converse)) ? fingerprints(o) : '' }
+            </div>
+            <div class="modal-footer">
+                ${modal_close_button}
+                <button type="button" class="btn btn-info refresh-contact"><i class="fa fa-refresh"> </i>${i18n_refresh}</button>
+                ${ (o.allow_contact_removal && o.is_roster_contact) ? remove_button : '' }
+
+            </div>
+        </div>
+    </div>
+`;

+ 1 - 0
webpack.html

@@ -6,6 +6,7 @@
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <meta name="description" content="Converse.js: A free chat client for your website" />
+    <script src="3rdparty/libsignal-protocol.js"></script>
     <link rel="manifest" href="./manifest.json">
     <link rel="shortcut icon" type="image/ico" href="favicon.ico"/>
 </head>