Replace modal templates with lit-html components

This commit is contained in:
JC Brand 2020-01-23 10:18:41 +01:00
parent a8104d7498
commit ad93407907
51 changed files with 1023 additions and 900 deletions

View File

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

16
package-lock.json generated
View File

@ -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"
}

View File

@ -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",

View File

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

View File

@ -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(modal.el.querySelector('form.add-xmpp-contact')).not.toBe(null);
expect(!_.isNull(modal.el.querySelector('form.add-xmpp-contact'))).toBeTruthy();
const input_jid = modal.el.querySelector('input[name="jid"]');
const input_name = modal.el.querySelector('input[name="name"]');
input_jid.value = 'someone@';
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;

View File

@ -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();
}));

View File

@ -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();
}));

View File

@ -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();

View File

@ -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);
}
},

View File

@ -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')

View File

@ -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());
}
});

View File

@ -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;

View File

@ -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

View File

@ -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': {}}))
})
);
}

View File

@ -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', {
});
}
});

View File

@ -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')
}));
},

View File

@ -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';
}
}

View File

@ -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);
}

View File

@ -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",

View File

@ -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)];
};
}

View File

@ -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>

View File

@ -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>
`;

View File

@ -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>
`;

View File

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

3
src/templates/alert.js Normal file
View File

@ -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>`

View File

@ -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>

View File

@ -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
src/templates/avatar.js Normal file
View File

@ -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
src/templates/buttons.js Normal file
View File

@ -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>`;

View File

@ -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>
`;

View File

@ -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>

View File

@ -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>
`;

View File

@ -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>

View File

@ -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>

View File

@ -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>
`;

View File

@ -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>

View File

@ -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>
`;

View File

@ -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>

View File

@ -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>
`;

View File

@ -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>

View File

@ -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
src/templates/profile.js Normal file
View File

@ -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>
`;

View File

@ -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>

View File

@ -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>
`;

View File

@ -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>

View File

@ -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
src/templates/prompt.js Normal file
View File

@ -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
src/templates/spinner.js Normal file
View File

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

View File

@ -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>

View File

@ -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>
`;

View File

@ -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>