Create an ElementView base modal and use it for all modals

Modals are now all web components and are opened by component name.
This commit is contained in:
JC Brand 2022-08-21 13:03:32 +02:00
parent 927add0707
commit fbe86e5af8
92 changed files with 2665 additions and 2754 deletions

View File

@ -106,6 +106,7 @@ module.exports = function(config) {
{ pattern: "src/plugins/register/tests/register.js", type: 'module' },
{ pattern: "src/plugins/roomslist/tests/roomslist.js", type: 'module' },
{ pattern: "src/plugins/rootview/tests/root.js", type: 'module' },
{ pattern: "src/plugins/rosterview/tests/add-contact-modal.js", type: 'module' },
{ pattern: "src/plugins/rosterview/tests/presence.js", type: 'module' },
{ pattern: "src/plugins/rosterview/tests/protocol.js", type: 'module' },
{ pattern: "src/plugins/rosterview/tests/roster.js", type: 'module' },

2353
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -87,7 +87,7 @@
"karma-jasmine": "^5.0.0",
"karma-jasmine-html-reporter": "^1.7.0",
"karma-webpack": "^5.0.0",
"lerna": "^5.1.8",
"lerna": "^5.5.1",
"mini-css-extract-plugin": "^2.6.0",
"minimist": "^1.2.6",
"po-loader": "0.6.1",

View File

@ -1,6 +1,5 @@
import RosterContact from './contact.js';
import log from "@converse/headless/log";
import sum from 'lodash-es/sum';
import { Collection } from "@converse/skeletor/src/collection";
import { Model } from "@converse/skeletor/src/model";
import { _converse, api, converse } from "@converse/headless/core";

View File

@ -1,14 +0,0 @@
import BootstrapModal from "plugins/modal/base.js";
import tpl_image_modal from "./templates/image.js";
export default BootstrapModal.extend({
id: 'image-modal',
toHTML () {
return tpl_image_modal({
'src': this.src,
'onload': ev => (ev.target.parentElement.style.height = `${ev.target.height}px`)
});
}
});

View File

@ -1,10 +0,0 @@
import BootstrapModal from "plugins/modal/base.js";
import tpl_message_versions_modal from "./templates/message-versions.js";
export default BootstrapModal.extend({
id: "message-versions-modal",
toHTML () {
return tpl_message_versions_modal(this.model);
}
});

View File

@ -1,20 +0,0 @@
import { html } from "lit";
import { __ } from 'i18n';
import { modal_close_button, modal_header_close_button } from "plugins/modal/templates/buttons.js"
export default (o) => {
return html`
<div class="modal-dialog fit-content" role="document">
<div class="modal-content fit-content">
<div class="modal-header">
<h4 class="modal-title" id="message-versions-modal-label">${__('Image: ')}<a target="_blank" rel="noopener" href="${o.src}">${o.src}</a></h4>
${modal_header_close_button}
</div>
<div class="modal-body modal-body--image fit-content">
<img class="chat-image" src="${o.src}" @load=${o.onload}>
</div>
<div class="modal-footer">${modal_close_button}</div>
</div>
</div>`;
}

View File

@ -1,20 +0,0 @@
import 'shared/components/message-versions.js';
import { __ } from 'i18n';
import { html } from "lit";
import { modal_close_button, modal_header_close_button } from "plugins/modal/templates/buttons.js"
export default (model) => html`
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="message-versions-modal-label">${__('Message versions')}</h4>
${modal_header_close_button}
</div>
<div class="modal-body">
<converse-message-versions .model=${model}></converse-message-versions>
</div>
<div class="modal-footer">${modal_close_button}</div>
</div>
</div>
`;

View File

@ -1,71 +0,0 @@
import avatar from 'shared/avatar/templates/avatar.js';
import { __ } from 'i18n';
import { html } from 'lit';
import { modal_close_button, modal_header_close_button } from 'plugins/modal/templates/buttons.js'
const remove_button = (o) => {
const i18n_remove_contact = __('Remove as contact');
return html`
<button type="button" @click="${o.removeContact}" class="btn btn-danger remove-contact">
<converse-icon
class="fas fa-trash-alt"
color="var(--text-color-lighten-15-percent)"
size="1em"
></converse-icon>
${i18n_remove_contact}
</button>
`;
}
export default (o) => {
const i18n_address = __('XMPP Address');
const i18n_email = __('Email');
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 avatar_data = {
'alt_text': i18n_profile,
'extra_classes': 'mb-3',
'height': '120',
'width': '120'
}
return 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 ? html`<div class="mb-4">${avatar(Object.assign(o, avatar_data))}</div>` : '' }
${ 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>` : '' }
<converse-omemo-fingerprints jid=${o.jid}></converse-omemo-fingerprints>
</div>
<div class="modal-footer">
${modal_close_button}
<button type="button" class="btn btn-info refresh-contact">
<converse-icon
class="fa fa-refresh"
color="var(--text-color-lighten-15-percent)"
size="1em"
></converse-icon>
${i18n_refresh}</button>
${ (o.allow_contact_removal && o.is_roster_contact) ? remove_button(o) : '' }
</div>
</div>
</div>
`;
}

View File

@ -1,4 +1,4 @@
import MUCBookmarkFormModal from './modal.js';
import './modal.js';
import { _converse, api, converse } from '@converse/headless/core';
const { u } = converse.env;
@ -34,6 +34,6 @@ export const bookmarkableChatRoomView = {
showBookmarkModal(ev) {
ev?.preventDefault();
const jid = this.model.get('jid');
api.modal.show(MUCBookmarkFormModal, { jid }, ev);
api.modal.show('converse-bookmark-form-modal', { jid }, ev);
}
};

View File

@ -1,19 +1,20 @@
import './form.js';
import BaseModal from "plugins/modal/base.js";
import tpl_modal from './templates/modal.js';
import BaseModal from "plugins/modal/modal.js";
import { html } from "lit";
import { __ } from 'i18n';
import { api } from "@converse/headless/core";
const MUCBookmarkFormModal = BaseModal.extend({
id: "converse-bookmark-modal",
export default class BookmarkFormModal extends BaseModal {
initialize (attrs) {
this.jid = attrs.jid;
this.affiliation = attrs.affiliation;
BaseModal.prototype.initialize.apply(this, arguments);
},
toHTML () {
return tpl_modal(this);
renderModal () {
return html`
<converse-muc-bookmark-form class="muc-form-container" jid="${this.jid}">
</converse-muc-bookmark-form>`;
}
});
export default MUCBookmarkFormModal;
getModalTitle () { // eslint-disable-line class-methods-use-this
return __('Bookmark');
}
}
api.elements.define('converse-bookmark-form-modal', BookmarkFormModal);

View File

@ -1,19 +0,0 @@
import { __ } from 'i18n';
import { html } from "lit";
import { modal_header_close_button } from "plugins/modal/templates/buttons.js"
export default (o) => {
const i18n_moderator_tools = __('Bookmark');
return 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">
<converse-muc-bookmark-form class="muc-form-container" jid="${o.jid}"></converse-muc-bookmark-form>
</div>
</div>
</div>`;
}

View File

@ -31,8 +31,8 @@ describe("A chat room", function () {
expect(toggle.title).toBe('Bookmark this groupchat');
toggle.click();
const modal = _converse.api.modal.get('converse-bookmark-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
const modal = _converse.api.modal.get('converse-bookmark-form-modal');
await u.waitUntil(() => u.isVisible(modal), 1000);
/* Client uploads data:
* --------------------
@ -66,13 +66,13 @@ describe("A chat room", function () {
* </iq>
*/
expect(view.model.get('bookmarked')).toBeFalsy();
const form = await u.waitUntil(() => modal.el.querySelector('.chatroom-form'));
const form = await u.waitUntil(() => modal.querySelector('.chatroom-form'));
form.querySelector('input[name="name"]').value = 'Play&apos;s the Thing';
form.querySelector('input[name="autojoin"]').checked = 'checked';
form.querySelector('input[name="nick"]').value = 'JC';
const IQ_stanzas = _converse.connection.IQ_stanzas;
modal.el.querySelector('converse-muc-bookmark-form .btn-primary').click();
modal.querySelector('converse-muc-bookmark-form .btn-primary').click();
const sent_stanza = await u.waitUntil(
() => IQ_stanzas.filter(s => sizzle('iq publish[node="storage:bookmarks"]', s).length).pop());
@ -250,16 +250,16 @@ describe("A chat room", function () {
bookmark_icon.click();
expect(view.showBookmarkModal).toHaveBeenCalled();
const modal = _converse.api.modal.get('converse-bookmark-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
const form = await u.waitUntil(() => modal.el.querySelector('.chatroom-form'));
const modal = _converse.api.modal.get('converse-bookmark-form-modal');
await u.waitUntil(() => u.isVisible(modal), 1000);
const form = await u.waitUntil(() => modal.querySelector('.chatroom-form'));
expect(form.querySelector('input[name="name"]').value).toBe('The Play');
expect(form.querySelector('input[name="autojoin"]').checked).toBeFalsy();
expect(form.querySelector('input[name="nick"]').value).toBe('Othello');
// Remove the bookmark
modal.el.querySelector('.button-remove').click();
modal.querySelector('.button-remove').click();
await u.waitUntil(() => view.querySelector('.chatbox-title__text .fa-bookmark') === null);
expect(_converse.bookmarks.length).toBe(0);

View File

@ -1,4 +1,4 @@
import MUCBookmarkFormModal from './modal.js';
import './modal.js';
import invokeMap from 'lodash-es/invokeMap';
import { Model } from '@converse/skeletor/src/model.js';
import { __ } from 'i18n';
@ -38,7 +38,7 @@ export async function removeBookmarkViaEvent (ev) {
export function addBookmarkViaEvent (ev) {
ev.preventDefault();
const jid = ev.currentTarget.getAttribute('data-room-jid');
api.modal.show(MUCBookmarkFormModal, { jid }, ev);
api.modal.show('converse-bookmark-form-modal', { jid }, ev);
}

View File

@ -1,4 +1,4 @@
import UserDetailsModal from 'modals/user-details.js';
import 'shared/modals/user-details.js';
import tpl_chatbox_head from './templates/chat-head.js';
import { CustomElement } from 'shared/components/element.js';
import { __ } from 'i18n';
@ -39,7 +39,7 @@ export default class ChatHeading extends CustomElement {
showUserDetailsModal (ev) {
ev.preventDefault();
api.modal.show(UserDetailsModal, { model: this.model }, ev);
api.modal.show('converse-user-details-modal', { model: this.model }, ev);
}
close (ev) {

View File

@ -343,9 +343,9 @@ describe("A Chat Message", function () {
expect(view.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
view.querySelector('.chat-msg__content .fa-edit').click();
const modal = _converse.api.modal.get('message-versions-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
const older_msgs = modal.el.querySelectorAll('.older-msg');
const modal = _converse.api.modal.get('converse-message-versions-modal');
await u.waitUntil(() => u.isVisible(modal), 1000);
const older_msgs = modal.querySelectorAll('.older-msg');
expect(older_msgs.length).toBe(2);
expect(older_msgs[0].textContent.includes('But soft, what light through yonder airlock breaks?')).toBe(true);
expect(view.model.messages.models.length).toBe(1);

View File

@ -3,8 +3,6 @@
const $msg = converse.env.$msg;
const u = converse.env.utils;
const Strophe = converse.env.Strophe;
const sizzle = converse.env.sizzle;
describe("The Controlbox", function () {
@ -124,10 +122,10 @@ describe("The Controlbox", function () {
await mock.openControlBox(_converse);
var cbview = _converse.chatboxviews.get('controlbox');
cbview.querySelector('.change-status').click()
const modal = _converse.api.modal.get('modal-status-change');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
modal.el.querySelector('label[for="radio-busy"]').click(); // Change status to "dnd"
modal.el.querySelector('[type="submit"]').click();
const modal = _converse.api.modal.get('converse-chat-status-modal');
await u.waitUntil(() => u.isVisible(modal), 1000);
modal.querySelector('label[for="radio-busy"]').click(); // Change status to "dnd"
modal.querySelector('[type="submit"]').click();
const sent_stanzas = _converse.connection.sent_stanzas;
const sent_presence = await u.waitUntil(() => sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).pop());
expect(Strophe.serialize(sent_presence)).toBe(
@ -149,12 +147,12 @@ describe("The Controlbox", function () {
await mock.openControlBox(_converse);
const cbview = _converse.chatboxviews.get('controlbox');
cbview.querySelector('.change-status').click()
const modal = _converse.api.modal.get('modal-status-change');
const modal = _converse.api.modal.get('converse-chat-status-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
await u.waitUntil(() => u.isVisible(modal), 1000);
const msg = 'I am happy';
modal.el.querySelector('input[name="status_message"]').value = msg;
modal.el.querySelector('[type="submit"]').click();
modal.querySelector('input[name="status_message"]').value = msg;
modal.querySelector('[type="submit"]').click();
const sent_stanzas = _converse.connection.sent_stanzas;
const sent_presence = await u.waitUntil(() => sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).pop());
expect(Strophe.serialize(sent_presence)).toBe(
@ -171,193 +169,3 @@ describe("The Controlbox", function () {
}));
});
});
describe("The 'Add Contact' widget", function () {
it("opens up an add modal when you click on it",
mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'all');
await mock.openControlBox(_converse);
const cbview = _converse.chatboxviews.get('controlbox');
cbview.querySelector('.add-contact').click()
const modal = _converse.api.modal.get('add-contact-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
expect(modal.el.querySelector('form.add-xmpp-contact')).not.toBe(null);
const input_jid = modal.el.querySelector('input[name="jid"]');
const input_name = modal.el.querySelector('input[name="name"]');
input_jid.value = 'someone@';
const evt = new Event('input');
input_jid.dispatchEvent(evt);
expect(modal.el.querySelector('.suggestion-box li').textContent).toBe('someone@montague.lit');
input_jid.value = 'someone@montague.lit';
input_name.value = 'Someone';
modal.el.querySelector('button[type="submit"]').click();
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>`);
}));
it("can be configured to not provide search suggestions",
mock.initConverse([], {'autocomplete_add_contact': false}, async function (_converse) {
await mock.waitForRoster(_converse, 'all', 0);
await mock.openControlBox(_converse);
const cbview = _converse.chatboxviews.get('controlbox');
cbview.querySelector('.add-contact').click()
const modal = _converse.api.modal.get('add-contact-modal');
expect(modal.jid_auto_complete).toBe(undefined);
expect(modal.name_auto_complete).toBe(undefined);
await u.waitUntil(() => u.isVisible(modal.el), 1000);
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(`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">`+
`<query xmlns="jabber:iq:roster"><item jid="someone@montague.lit"/></query>`+
`</iq>`
);
}));
it("integrates with xhr_user_search_url to search for contacts",
mock.initConverse([], { 'xhr_user_search_url': 'http://example.org/?' },
async function (_converse) {
await mock.waitForRoster(_converse, 'all', 0);
class MockXHR extends XMLHttpRequest {
open () {} // eslint-disable-line
responseText = ''
send () {
this.responseText = JSON.stringify([
{"jid": "marty@mcfly.net", "fullname": "Marty McFly"},
{"jid": "doc@brown.com", "fullname": "Doc Brown"}
]);
this.onload();
}
}
const XMLHttpRequestBackup = window.XMLHttpRequest;
window.XMLHttpRequest = MockXHR;
await mock.openControlBox(_converse);
const cbview = _converse.chatboxviews.get('controlbox');
cbview.querySelector('.add-contact').click()
const modal = _converse.api.modal.get('add-contact-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
// We only have autocomplete for the name input
expect(modal.jid_auto_complete).toBe(undefined);
expect(modal.name_auto_complete instanceof _converse.AutoComplete).toBe(true);
const input_el = modal.el.querySelector('input[name="name"]');
input_el.value = 'marty';
input_el.dispatchEvent(new Event('input'));
await u.waitUntil(() => modal.el.querySelector('.suggestion-box li'), 1000);
expect(modal.el.querySelectorAll('.suggestion-box li').length).toBe(1);
const suggestion = modal.el.querySelector('.suggestion-box li');
expect(suggestion.textContent).toBe('Marty McFly');
// Mock selection
modal.name_auto_complete.select(suggestion);
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();
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;
}));
it("can be configured to not provide search suggestions for XHR search results",
mock.initConverse([],
{ 'autocomplete_add_contact': false,
'xhr_user_search_url': 'http://example.org/?' },
async function (_converse) {
await mock.waitForRoster(_converse, 'all');
await mock.openControlBox(_converse);
class MockXHR extends XMLHttpRequest {
open () {} // eslint-disable-line
responseText = ''
send () {
const value = modal.el.querySelector('input[name="name"]').value;
if (value === 'existing') {
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
this.responseText = JSON.stringify([{"jid": contact_jid, "fullname": mock.cur_names[0]}]);
} else if (value === 'romeo') {
this.responseText = JSON.stringify([{"jid": "romeo@montague.lit", "fullname": "Romeo Montague"}]);
} else if (value === 'ambiguous') {
this.responseText = JSON.stringify([
{"jid": "marty@mcfly.net", "fullname": "Marty McFly"},
{"jid": "doc@brown.com", "fullname": "Doc Brown"}
]);
} else if (value === 'insufficient') {
this.responseText = JSON.stringify([]);
} else {
this.responseText = JSON.stringify([{"jid": "marty@mcfly.net", "fullname": "Marty McFly"}]);
}
this.onload();
}
}
const XMLHttpRequestBackup = window.XMLHttpRequest;
window.XMLHttpRequest = MockXHR;
const cbview = _converse.chatboxviews.get('controlbox');
cbview.querySelector('.add-contact').click()
const modal = _converse.api.modal.get('add-contact-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
expect(modal.jid_auto_complete).toBe(undefined);
expect(modal.name_auto_complete).toBe(undefined);
const input_el = modal.el.querySelector('input[name="name"]');
input_el.value = 'ambiguous';
modal.el.querySelector('button[type="submit"]').click();
let feedback_el = modal.el.querySelector('.invalid-feedback');
expect(feedback_el.textContent).toBe('Sorry, could not find a contact with that name');
feedback_el.textContent = '';
input_el.value = 'insufficient';
modal.el.querySelector('button[type="submit"]').click();
feedback_el = modal.el.querySelector('.invalid-feedback');
expect(feedback_el.textContent).toBe('Sorry, could not find a contact with that name');
feedback_el.textContent = '';
input_el.value = 'existing';
modal.el.querySelector('button[type="submit"]').click();
feedback_el = modal.el.querySelector('.invalid-feedback');
expect(feedback_el.textContent).toBe('This contact has already been added');
input_el.value = 'Marty McFly';
modal.el.querySelector('button[type="submit"]').click();
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

@ -1,19 +1,23 @@
import BootstrapModal from "./base.js";
import BaseModal from "plugins/modal/modal.js";
import tpl_alert_modal from "./templates/alert.js";
import { __ } from 'i18n';
import { api } from "@converse/headless/core";
const Alert = BootstrapModal.extend({
id: 'alert-modal',
export default class Alert extends BaseModal {
initialize () {
BootstrapModal.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change', this.render)
},
toHTML () {
return tpl_alert_modal(Object.assign({__}, this.model.toJSON()));
super.initialize();
this.listenTo(this.model, 'change', () => this.render())
this.addEventListener('hide.bs.modal', () => this.remove(), false);
}
});
export default Alert;
renderModal () {
return tpl_alert_modal(this.model.toJSON());
}
getModalTitle () {
return this.model.get('title');
}
}
api.elements.define('converse-alert-modal', Alert);

View File

@ -1,9 +1,9 @@
import Alert from './alert.js';
import './alert.js';
import Confirm from './confirm.js';
import { Model } from '@converse/skeletor/src/model.js';
let modals = [];
let modals_map = {};
const modal_api = {
/**
@ -17,13 +17,20 @@ const modal_api = {
* Will create a new instance of that class if an existing one isn't
* found.
* @param { Class } ModalClass
* @param { Object } [properties] - Optional properties that will be
* set on a newly created modal instance (if no pre-existing modal was
* found).
* @param { Object } [properties] - Optional properties that will be set on a newly created modal instance.
* @param { Event } [event] - The DOM event that causes the modal to be shown.
*/
show (ModalClass, properties, ev) {
const modal = this.get(ModalClass.id) || this.create(ModalClass, properties);
show (name, properties, ev) {
let modal;
if (typeof name === 'string') {
modal = this.get(name) ?? this.create(name, properties);
Object.assign(modal, properties);
} else {
// Legacy...
const ModalClass = name;
const id = ModalClass.id ?? properties.id;
modal = this.get(id) ?? this.create(ModalClass, properties);
}
modal.show(ev);
return modal;
},
@ -33,28 +40,44 @@ const modal_api = {
* @param { String } id
*/
get (id) {
return modals.filter(m => m.id == id).pop();
return modals_map[id] ?? modals.filter(m => m.id == id).pop();
},
/**
* Create a modal of the passed-in type.
* @param { Class } ModalClass
* @param { String } name
* @param { Object } [properties] - Optional properties that will be
* set on the modal instance.
*/
create (ModalClass, properties) {
const modal = new ModalClass(properties);
modals.push(modal);
create (name, properties) {
let modal;
if (typeof name === 'string') {
const ModalClass = customElements.get(name);
modal = modals_map[name] = new ModalClass(properties);
} else {
// Legacy...
const ModalClass = name;
modal = new ModalClass(properties);
modals.push(modal);
}
return modal;
},
/**
* Remove a particular modal
* @param { View } modal
* @param { String } name
*/
remove (modal) {
modals = modals.filter(m => m !== modal);
modal.remove();
remove (name) {
let modal;
if (typeof name === 'string') {
modal = modals_map[name];
delete modals_map[name];
} else {
// Legacy...
modal = name;
modals = modals.filter(m => m !== modal);
}
modal?.remove();
},
/**
@ -63,6 +86,7 @@ const modal_api = {
removeAll () {
modals.forEach(m => m.remove());
modals = [];
modals_map = {};
}
},
@ -157,7 +181,7 @@ const modal_api = {
'level': level,
'type': 'alert'
})
modal_api.modal.show(Alert, {model});
modal_api.modal.show('converse-alert-modal', { model });
}
}

View File

@ -1,6 +1,6 @@
import bootstrap from "bootstrap.native";
import log from "@converse/headless/log";
import tpl_alert_component from "templates/alert.js";
import tpl_alert_component from "./templates/modal-alert.js";
import { View } from '@converse/skeletor/src/view.js';
import { api, converse } from "@converse/headless/core";
import { render } from 'lit';

View File

@ -1,35 +1,32 @@
import BootstrapModal from './base.js';
import BaseModal from "plugins/modal/modal.js";
import tpl_prompt from "./templates/prompt.js";
import { getOpenPromise } from '@converse/openpromise';
import { api } from "@converse/headless/core";
export default class Confirm extends BaseModal {
const Confirm = BootstrapModal.extend({
id: 'confirm-modal',
events: {
'submit .confirm': 'onConfimation'
},
constructor (options) {
super(options);
this.confirmation = getOpenPromise();
}
initialize () {
this.confirmation = getOpenPromise();
BootstrapModal.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change', this.render)
this.el.addEventListener('closed.bs.modal', () => this.confirmation.reject(), false);
},
super.initialize();
this.listenTo(this.model, 'change', () => this.render())
this.addEventListener('hide.bs.modal', () => {
if (!this.confirmation.isResolved) {
this.confirmation.reject()
}
}, false);
}
toHTML () {
return tpl_prompt(this.model.toJSON());
},
renderModal () {
return tpl_prompt(this);
}
afterRender () {
if (!this.close_handler_registered) {
this.el.addEventListener('closed.bs.modal', () => {
if (!this.confirmation.isResolved) {
this.confirmation.reject()
}
}, false);
this.close_handler_registered = true;
}
},
getModalTitle () {
this.model.get('title');
}
onConfimation (ev) {
ev.preventDefault();
@ -53,6 +50,6 @@ const Confirm = BootstrapModal.extend({
this.confirmation.resolve(fields);
this.modal.hide();
}
});
}
export default Confirm;
api.elements.define('converse-confirm-modal', Confirm);

View File

@ -0,0 +1,70 @@
import bootstrap from "bootstrap.native";
import tpl_modal from './templates/modal.js';
import { ElementView } from '@converse/skeletor/src/element.js';
import { getOpenPromise } from '@converse/openpromise';
import './styles/_modal.scss';
class BaseModal extends ElementView {
constructor (options) {
super();
this.className = 'modal';
this.initialized = getOpenPromise();
// Allow properties to be set via passed in options
Object.assign(this, options);
setTimeout(() => this.insertIntoDOM());
}
initialize () {
this.modal = new bootstrap.Modal(this, {
backdrop: true,
keyboard: true
});
this.addEventListener('hide.bs.modal', () => this.onHide(), false);
this.initialized.resolve();
this.render()
}
toHTML () {
return tpl_modal(this);
}
getModalTitle () { // eslint-disable-line class-methods-use-this
// Intended to be overwritten
return '';
}
switchTab (ev) {
ev?.stopPropagation();
ev?.preventDefault();
this.tab = ev.target.getAttribute('data-name');
this.render();
}
onHide () {
this.modal.hide();
}
insertIntoDOM () {
const container_el = document.querySelector("#converse-modals");
container_el.insertAdjacentElement('beforeEnd', this);
}
alert (message, type='primary') {
this.model.set('alert', { message, type });
setTimeout(() => {
this.model.set('alert', undefined);
}, 5000);
}
async show () {
await this.initialized;
this.modal.show();
this.render();
}
}
export default BaseModal;

View File

@ -55,7 +55,6 @@
}
}
}
&.fit-content {
box-sizing: content-box;
@ -64,12 +63,6 @@
}
}
}
.modal-body--image {
.chat-image {
max-height: 99%;
max-width: 100%;
}
}
.modal-footer {
justify-content: flex-start;
}
@ -103,43 +96,5 @@
.btn {
font-weight: normal;
}
#user-profile-modal {
.profile-form {
label {
font-weight: bold;
}
}
.fingerprint-removal {
label {
display: flex;
padding: 0.75rem 1.25rem;
}
}
.list-group-item {
display: flex;
justify-content: left;
font-size: 95%;
input[type="checkbox"] {
margin-right: 1em;
}
}
}
.fingerprints {
width: 100%;
margin-bottom: 1em;
}
.fingerprint-trust {
display: flex;
justify-content: space-between;
font-size: 95%;
.fingerprint {
margin-left: 1em;
}
}
}
}

View File

@ -1,17 +1,8 @@
import { html } from "lit";
import { modal_header_close_button } from "./buttons.js"
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 class="modal-body">
<span class="modal-alert"></span>
${ o.messages.map(message => html`<p>${message}</p>`) }
</div>`;

View File

@ -0,0 +1,29 @@
import tpl_alert_component from "./modal-alert.js";
import { html } from "lit";
import { modal_close_button, modal_header_close_button } from "plugins/modal/templates/buttons.js";
export default (el) => {
const alert = el.model?.get('alert');
const level = el.model?.get('level') ?? '';
return html`
<div class="modal-dialog" role="document" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-content">
<div class="modal-header ${level}">
<h5 class="modal-title">${el.getModalTitle()}</h5>
${modal_header_close_button}
</div>
<div class="modal-body">
<span class="modal-alert">
${ alert ? tpl_alert_component({'type': `alert-${alert.type}`, 'message': alert.message}) : ''}
</span>
${ el.renderModal?.() ?? '' }
</div>
<div class="modal-footer">
${ modal_close_button }
${ el.renderModalFooter?.() ?? '' }
</div>
</div>
</div>
`;
}

View File

@ -15,29 +15,16 @@ const tpl_field = (f) => html`
</div>
`;
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>
${ o.fields.map(f => tpl_field(f)) }
<div class="form-group">
<button type="submit" class="btn btn-primary">${__('OK')}</button>
<input type="button" class="btn btn-secondary" data-dismiss="modal" value="${__('Cancel')}"/>
</div>
</form>
</div>
</div>
</div>
`;
export default (el) => {
return html`
<form class="converse-form converse-form--modal confirm" action="#" @submit=${ev => el.onConfimation(ev)}>
<div class="form-group">
${ el.model.get('messages')?.map(message => html`<p>${message}</p>`) }
</div>
${ el.model.get('fields')?.map(f => tpl_field(f)) }
<div class="form-group">
<button type="submit" class="btn btn-primary">${__('OK')}</button>
<input type="button" class="btn btn-secondary" data-dismiss="modal" value="${__('Cancel')}"/>
</div>
</form>`;
}

View File

@ -1,6 +1,6 @@
import MUCInviteModal from './modals/muc-invite.js';
import NicknameModal from './modals/nickname.js';
import RoomDetailsModal from './modals/muc-details.js';
import './modals/muc-details.js';
import './modals/muc-invite.js';
import './modals/nickname.js';
import tpl_muc_head from './templates/muc-head.js';
import { CustomElement } from 'shared/components/element.js';
import { Model } from '@converse/skeletor/src/model.js';
@ -48,12 +48,12 @@ export default class MUCHeading extends CustomElement {
showRoomDetailsModal (ev) {
ev.preventDefault();
api.modal.show(RoomDetailsModal, { 'model': this.model }, ev);
api.modal.show('converse-muc-details-modal', { 'model': this.model }, ev);
}
showInviteModal (ev) {
ev.preventDefault();
api.modal.show(MUCInviteModal, { 'model': new Model(), 'chatroomview': this }, ev);
api.modal.show('converse-muc-invite-modal', { 'model': new Model(), 'chatroomview': this }, ev);
}
toggleTopic (ev) {
@ -104,7 +104,7 @@ export default class MUCHeading extends CustomElement {
buttons.push({
'i18n_text': __('Nickname'),
'i18n_title': __("Change the nickname you're using in this groupchat"),
'handler': ev => api.modal.show(NicknameModal, { 'model': this.model }, ev),
'handler': ev => api.modal.show('converse-muc-nickname-modal', { 'model': this.model }, ev),
'a_class': 'open-nickname-modal',
'icon_class': 'fa-smile',
'name': 'nickname'

View File

@ -1,5 +1,5 @@
import tpl_add_muc from "../templates/add-muc.js";
import BootstrapModal from "plugins/modal/base.js";
import tpl_add_muc from "./templates/add-muc.js";
import BaseModal from "plugins/modal/modal.js";
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
@ -9,43 +9,27 @@ const u = converse.env.utils;
const { Strophe } = converse.env;
export default BootstrapModal.extend({
persistent: true,
id: 'add-chatroom-modal',
events: {
'submit form.add-chatroom': 'openChatRoom',
'keyup .roomjid-input': 'checkRoomidPolicy',
'change .roomjid-input': 'checkRoomidPolicy'
},
export default class AddMUCModal extends BaseModal {
initialize () {
BootstrapModal.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change:muc_domain', this.render);
super.initialize();
this.listenTo(this.model, 'change:muc_domain', () => this.render());
this.muc_roomid_policy_error_msg = null;
},
toHTML () {
let placeholder = '';
if (!api.settings.get('locked_muc_domain')) {
const muc_domain = this.model.get('muc_domain') || api.settings.get('muc_domain');
placeholder = muc_domain ? `name@${muc_domain}` : __('name@conference.example.org');
}
return tpl_add_muc(Object.assign(this.model.toJSON(), {
'label_room_address': api.settings.get('muc_domain') ? __('Groupchat name') : __('Groupchat address'),
'chatroom_placeholder': placeholder,
'muc_roomid_policy_error_msg': this.muc_roomid_policy_error_msg,
'muc_roomid_policy_hint': api.settings.get('muc_roomid_policy_hint')
}));
},
afterRender () {
this.el.addEventListener('shown.bs.modal', () => {
this.el.querySelector('input[name="chatroom"]').focus();
this.render();
this.addEventListener('shown.bs.modal', () => {
this.querySelector('input[name="chatroom"]').focus();
}, false);
},
}
parseRoomDataFromEvent (form) {
renderModal () {
return tpl_add_muc(this);
}
getModalTitle () { // eslint-disable-line class-methods-use-this
return __('Enter a new Groupchat');
}
parseRoomDataFromEvent (form) { // eslint-disable-line class-methods-use-this
const data = new FormData(form);
const jid = data.get('chatroom')?.trim();
let nick;
@ -61,10 +45,12 @@ export default BootstrapModal.extend({
'jid': jid,
'nick': nick
}
},
}
openChatRoom (ev) {
ev.preventDefault();
if (this.checkRoomidPolicy()) return;
const data = this.parseRoomDataFromEvent(ev.target);
if (data.nick === "") {
// Make sure defaults apply if no nick is provided.
@ -77,14 +63,15 @@ export default BootstrapModal.extend({
jid = data.jid
this.model.setDomain(jid);
}
api.rooms.open(jid, Object.assign(data, {jid}), true);
this.modal.hide();
ev.target.reset();
},
this.modal.hide();
}
checkRoomidPolicy () {
if (api.settings.get('muc_roomid_policy') && api.settings.get('muc_domain')) {
let jid = this.el.querySelector('.roomjid-input').value;
let jid = this.querySelector('converse-autocomplete input').value;
if (api.settings.get('locked_muc_domain') || !u.isValidJID(jid)) {
jid = `${Strophe.escapeNode(jid)}@${api.settings.get('muc_domain')}`;
}
@ -95,8 +82,11 @@ export default BootstrapModal.extend({
this.muc_roomid_policy_error_msg = null;
} else {
this.muc_roomid_policy_error_msg = __('Groupchat id is invalid.');
return true;
}
this.render();
}
}
});
}
api.elements.define('converse-add-muc-modal', AddMUCModal);

View File

@ -1,20 +1,24 @@
import '../modtools.js';
import BaseModal from "plugins/modal/base.js";
import tpl_moderator_tools from './templates/moderator-tools.js';
import BaseModal from "plugins/modal/modal.js";
import { __ } from 'i18n';
import { api } from "@converse/headless/core";
import { html } from 'lit';
const ModeratorToolsModal = BaseModal.extend({
id: "converse-modtools-modal",
persistent: true,
export default class ModeratorToolsModal extends BaseModal {
initialize (attrs) {
this.jid = attrs.jid;
this.affiliation = attrs.affiliation;
BaseModal.prototype.initialize.apply(this, arguments);
},
toHTML () {
return tpl_moderator_tools(this);
constructor (options) {
super(options);
this.id = "converse-modtools-modal";
}
});
export default ModeratorToolsModal;
renderModal () {
return html`<converse-modtools jid=${this.jid} affiliation=${this.affiliation}></converse-modtools>`;
}
getModalTitle () { // eslint-disable-line class-methods-use-this
return __('Moderator Tools');
}
}
api.elements.define('converse-modtools-modal', ModeratorToolsModal);

View File

@ -1,21 +1,29 @@
import BaseModal from "plugins/modal/base.js";
import BaseModal from "plugins/modal/modal.js";
import tpl_muc_details from "./templates/muc-details.js";
import { __ } from 'i18n';
import { api } from "@converse/headless/core";
import '../styles/muc-details.scss';
import '../styles/muc-details-modal.scss';
export default BaseModal.extend({
id: "muc-details-modal",
export default class MUCDetailsModal extends BaseModal {
initialize () {
BaseModal.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model.features, 'change', this.render);
this.listenTo(this.model.occupants, 'add', this.render);
this.listenTo(this.model.occupants, 'change', this.render);
},
super.initialize();
this.listenTo(this.model, 'change', () => this.render());
this.listenTo(this.model.features, 'change', () => this.render());
this.listenTo(this.model.occupants, 'add', () => this.render());
this.listenTo(this.model.occupants, 'change', () => this.render());
}
toHTML () {
renderModal () {
return tpl_muc_details(this.model);
}
});
getModalTitle () { // eslint-disable-line class-methods-use-this
return __('Groupchat info for %1$s', this.model.getDisplayName());
}
}
api.elements.define('converse-muc-details-modal', MUCDetailsModal);

View File

@ -1,45 +1,35 @@
import 'shared/autocomplete/index.js';
import BaseModal from "plugins/modal/base.js";
import BaseModal from "plugins/modal/modal.js";
import tpl_muc_invite_modal from "./templates/muc-invite.js";
import { _converse, converse } from "@converse/headless/core";
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
const u = converse.env.utils;
export default BaseModal.extend({
id: "muc-invite-modal",
export default class MUCInviteModal extends BaseModal {
initialize () {
BaseModal.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change', this.render);
this.initInviteWidget();
},
super.initialize();
this.listenTo(this.model, 'change', () => this.render());
}
toHTML () {
return tpl_muc_invite_modal(Object.assign(
this.model.toJSON(), {
'submitInviteForm': ev => this.submitInviteForm(ev)
})
);
},
renderModal () {
return tpl_muc_invite_modal(this);
}
initInviteWidget () {
if (this.invite_auto_complete) {
this.invite_auto_complete.destroy();
}
const list = _converse.roster.map(i => ({'label': i.getDisplayName(), 'value': i.get('jid')}));
const el = this.el.querySelector('.suggestion-box').parentElement;
this.invite_auto_complete = new _converse.AutoComplete(el, {
'min_chars': 1,
'list': list
});
},
getModalTitle () { // eslint-disable-line class-methods-use-this
return __('Invite someone to this groupchat');
}
getAutoCompleteList () { // eslint-disable-line class-methods-use-this
return _converse.roster.map(i => ({'label': i.getDisplayName(), 'value': i.get('jid')}));
}
submitInviteForm (ev) {
ev.preventDefault();
// TODO: Add support for sending an invite to multiple JIDs
const data = new FormData(ev.target);
const jid = data.get('invitee_jids');
const jid = data.get('invitee_jids')?.trim();
const reason = data.get('reason');
if (u.isValidJID(jid)) {
// TODO: Create and use API here
@ -49,4 +39,6 @@ export default BaseModal.extend({
this.model.set({'invalid_invite_jid': true});
}
}
});
}
api.elements.define('converse-muc-invite-modal', MUCInviteModal);

View File

@ -1,8 +1,8 @@
import BootstrapModal from "plugins/modal/base.js";
import BaseModal from "plugins/modal/modal.js";
import head from "lodash-es/head";
import log from "@converse/headless/log";
import tpl_muc_list from "../templates/muc-list.js";
import tpl_muc_description from "../templates/muc-description.js";
import tpl_muc_list from "../templates/muc-list.js";
import tpl_spinner from "templates/spinner.js";
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
@ -65,28 +65,25 @@ function toggleRoomInfo (ev) {
}
export default BootstrapModal.extend({
id: "muc-list-modal",
persistent: true,
export default class MUCListModal extends BaseModal {
initialize () {
constructor (options) {
super(options);
this.items = [];
this.loading_items = false;
}
BootstrapModal.prototype.initialize.apply(this, arguments);
initialize () {
super.initialize();
this.listenTo(this.model, 'change:muc_domain', this.onDomainChange);
this.listenTo(this.model, 'change:feedback_text', () => this.render());
this.el.addEventListener('shown.bs.modal', () => api.settings.get('locked_muc_domain')
? this.updateRoomsList()
: this.el.querySelector('input[name="server"]').focus()
);
this.addEventListener('shown.bs.modal', () => api.settings.get('locked_muc_domain') && this.updateRoomsList());
this.model.save('feedback_text', '');
},
}
toHTML () {
renderModal () {
return tpl_muc_list(
Object.assign(this.model.toJSON(), {
'show_form': !api.settings.get('locked_muc_domain'),
@ -98,7 +95,11 @@ export default BootstrapModal.extend({
'submitForm': ev => this.showRooms(ev),
'toggleRoomInfo': ev => this.toggleRoomInfo(ev)
}));
},
}
getModalTitle () { // eslint-disable-line class-methods-use-this
return __('Query for Groupchats');
}
openRoom (ev) {
ev.preventDefault();
@ -106,16 +107,16 @@ export default BootstrapModal.extend({
const name = ev.target.getAttribute('data-room-name');
this.modal.hide();
api.rooms.open(jid, {'name': name}, true);
},
}
toggleRoomInfo (ev) {
toggleRoomInfo (ev) { // eslint-disable-line
ev.preventDefault();
toggleRoomInfo(ev);
},
}
onDomainChange () {
api.settings.get('auto_list_rooms') && this.updateRoomsList();
},
}
/**
* Handle the IQ stanza returned from the server, containing
@ -136,7 +137,7 @@ export default BootstrapModal.extend({
}
this.render();
return true;
},
}
/**
* Send an IQ stanza to the server asking for all groupchats
@ -152,7 +153,7 @@ export default BootstrapModal.extend({
api.sendIQ(iq)
.then(iq => this.onRoomsFound(iq))
.catch(() => this.onRoomsFound())
},
}
showRooms (ev) {
ev.preventDefault();
@ -162,13 +163,15 @@ export default BootstrapModal.extend({
const data = new FormData(ev.target);
this.model.setDomain(data.get('server'));
this.updateRoomsList();
},
}
setDomainFromEvent (ev) {
this.model.setDomain(ev.target.value);
},
}
setNick (ev) {
this.model.save({nick: ev.target.value});
}
});
}
api.elements.define('converse-muc-list-modal', MUCListModal);

View File

@ -1,15 +1,17 @@
import tpl_nickname from "./templates/nickname.js";
import BaseModal from "plugins/modal/base.js";
import BaseModal from "plugins/modal/modal.js";
import { __ } from 'i18n';
import { api } from "@converse/headless/core.js";
import { html } from 'lit';
export default BaseModal.extend({
id: 'change-nickname-modal',
export default class MUCNicknameModal extends BaseModal {
initialize (attrs) {
this.model = attrs.model;
BaseModal.prototype.initialize.apply(this, arguments);
},
renderModal () {
return html`<converse-muc-nickname-form jid="${this.model.get('jid')}"></converse-muc-nickname-form>`;
}
toHTML () {
return tpl_nickname(this);
},
});
getModalTitle () { // eslint-disable-line class-methods-use-this
return __('Change your nickname');
}
}
api.elements.define('converse-muc-nickname-modal', MUCNicknameModal);

View File

@ -1,16 +1,14 @@
import BaseModal from "plugins/modal/base.js";
import BaseModal from "plugins/modal/modal.js";
import tpl_occupant_modal from "./templates/occupant.js";
import { _converse, api } from "@converse/headless/core";
const OccupantModal = BaseModal.extend({
id: "muc-occupant",
export default class OccupantModal extends BaseModal {
initialize () {
BaseModal.prototype.initialize.apply(this, arguments);
if (this.model) {
this.listenTo(this.model, 'change', this.render);
}
super.initialize()
const model = this.model ?? this.message;
this.listenTo(model, 'change', () => this.render());
/**
* Triggered once the OccupantModal has been initialized
* @event _converse#occupantModalInitialized
@ -18,7 +16,7 @@ const OccupantModal = BaseModal.extend({
* @example _converse.api.listen.on('occupantModalInitialized', data);
*/
api.trigger('occupantModalInitialized', { 'model': this.model, 'message': this.message });
},
}
getVcard () {
const model = this.model ?? this.message;
@ -27,22 +25,24 @@ const OccupantModal = BaseModal.extend({
}
const jid = model?.get('jid') || model?.get('from');
return jid ? _converse.vcards.get(jid) : null;
},
}
toHTML () {
renderModal () {
const model = this.model ?? this.message;
const jid = model?.get('jid');
const vcard = this.getVcard();
const display_name = model?.getDisplayName();
const nick = model.get('nick');
const occupant_id = model.get('occupant_id');
const role = this.model?.get('role');
const affiliation = this.model?.get('affiliation');
const hats = this.model?.get('hats')?.length ? this.model.get('hats') : null;
return tpl_occupant_modal({ jid, vcard, display_name, nick, occupant_id, role, affiliation, hats });
return tpl_occupant_modal({ jid, vcard, nick, occupant_id, role, affiliation, hats });
}
});
_converse.OccupantModal = OccupantModal;
getModalTitle () { // eslint-disable-line class-methods-use-this
const model = this.model ?? this.message;
return model?.getDisplayName();
}
}
export default OccupantModal;
api.elements.define('converse-muc-occupant-modal', OccupantModal);

View File

@ -0,0 +1,57 @@
import DOMPurify from 'dompurify';
import { __ } from 'i18n';
import { api } from '@converse/headless/core.js';
import { html } from "lit";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { getAutoCompleteList } from "../../search.js";
const nickname_input = (el) => {
const i18n_nickname = __('Nickname');
const i18n_required_field = __('This field is required');
return html`
<div class="form-group" >
<label for="nickname">${i18n_nickname}:</label>
<input type="text"
title="${i18n_required_field}"
required="required"
name="nickname"
value="${el.model.get('nick') || ''}"
class="form-control"/>
</div>
`;
}
export default (el) => {
const i18n_join = __('Join');
const muc_domain = el.model.get('muc_domain') || api.settings.get('muc_domain');
let placeholder = '';
if (!api.settings.get('locked_muc_domain')) {
placeholder = muc_domain ? `name@${muc_domain}` : __('name@conference.example.org');
}
const label_room_address = muc_domain ? __('Groupchat name') : __('Groupchat address');
const muc_roomid_policy_error_msg = el.muc_roomid_policy_error_msg;
const muc_roomid_policy_hint = api.settings.get('muc_roomid_policy_hint');
return html`
<form class="converse-form add-chatroom" @submit=${(ev) => el.openChatRoom(ev)}>
<div class="form-group">
<label for="chatroom">${label_room_address}:</label>
${ (muc_roomid_policy_error_msg) ? html`<label class="roomid-policy-error">${muc_roomid_policy_error_msg}</label>` : '' }
<converse-autocomplete
.getAutoCompleteList=${getAutoCompleteList}
?autofocus=${true}
min_chars="3"
position="below"
placeholder="${placeholder}"
class="add-muc-autocomplete"
name="chatroom">
</converse-autocomplete>
</div>
${ muc_roomid_policy_hint ? html`<div class="form-group">${unsafeHTML(DOMPurify.sanitize(muc_roomid_policy_hint, {'ALLOWED_TAGS': ['b', 'br', 'em']}))}</div>` : '' }
${ !api.settings.get('locked_muc_nickname') ? nickname_input(el) : '' }
<input type="submit" class="btn btn-primary" name="join" value="${i18n_join || ''}" ?disabled=${muc_roomid_policy_error_msg}/>
</form>
`;
}

View File

@ -1,19 +0,0 @@
import { __ } from 'i18n';
import { html } from "lit";
import { modal_header_close_button } from "plugins/modal/templates/buttons.js"
export default (o) => {
const i18n_moderator_tools = __('Moderator Tools');
return 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">
<converse-modtools jid=${o.jid} affiliation=${o.affiliation}></converse-modtools>
</div>
</div>
</div>`;
}

View File

@ -1,6 +1,5 @@
import { __ } from 'i18n';
import { html } from "lit";
import { modal_close_button, modal_header_close_button } from "plugins/modal/templates/buttons.js";
const subject = (o) => {
@ -16,7 +15,6 @@ const subject = (o) => {
export default (model) => {
const o = model.toJSON();
const config = model.config.toJSON();
const display_name = __('Groupchat info for %1$s', model.getDisplayName());
const features = model.features.toJSON();
const num_occupants = model.occupants.filter(o => o.get('show') !== 'offline').length;
@ -51,43 +49,30 @@ export default (model) => {
const i18n_temporary = __('Temporary');
const i18n_temporary_help = __('This groupchat will disappear once the last person leaves');
return html`
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="muc-details-modal-label">${display_name}</h5>
${modal_header_close_button}
<div class="room-info">
<p class="room-info"><strong>${i18n_name}</strong>: ${o.name}</p>
<p class="room-info"><strong>${i18n_address}</strong>: <converse-rich-text text="xmpp:${o.jid}?join"></converse-rich-text></p>
<p class="room-info"><strong>${i18n_desc}</strong>: <converse-rich-text text="${config.description}" render_styling></converse-rich-text></p>
${ (o.subject) ? subject(o) : '' }
<p class="room-info"><strong>${i18n_online_users}</strong>: ${num_occupants}</p>
<p class="room-info"><strong>${i18n_features}</strong>:
<div class="chatroom-features">
<ul class="features-list">
${ features.passwordprotected ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-lock"></converse-icon>${i18n_password_protected} - <em>${i18n_password_help}</em></li>` : '' }
${ features.unsecured ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-unlock"></converse-icon>${i18n_no_password_required} - <em>${i18n_no_pass_help}</em></li>` : '' }
${ features.hidden ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-eye-slash"></converse-icon>${i18n_hidden} - <em>${i18n_hidden_help}</em></li>` : '' }
${ features.public_room ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-eye"></converse-icon>${i18n_public} - <em>${o.__('This groupchat is publicly searchable') }</em></li>` : '' }
${ features.membersonly ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-address-book"></converse-icon>${i18n_members_only} - <em>${i18n_members_help}</em></li>` : '' }
${ features.open ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-globe"></converse-icon>${i18n_open} - <em>${i18n_open_help}</em></li>` : '' }
${ features.persistent ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-save"></converse-icon>${i18n_persistent} - <em>${i18n_persistent_help}</em></li>` : '' }
${ features.temporary ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-snowflake"></converse-icon>${i18n_temporary} - <em>${i18n_temporary_help}</em></li>` : '' }
${ features.nonanonymous ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-id-card"></converse-icon>${i18n_not_anonymous} - <em>${i18n_not_anonymous_help}</em></li>` : '' }
${ features.semianonymous ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-user-secret"></converse-icon>${i18n_semi_anon} - <em>${i18n_semi_anon_help}</em></li>` : '' }
${ features.moderated ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-gavel"></converse-icon>${i18n_moderated} - <em>${i18n_moderated_help}</em></li>` : '' }
${ features.unmoderated ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-info-circle"></converse-icon>${i18n_not_moderated} - <em>${i18n_not_moderated_help}</em></li>` : '' }
${ features.mam_enabled ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-database"></converse-icon>${i18n_archiving} - <em>${i18n_archiving_help}</em></li>` : '' }
</ul>
</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>: <converse-rich-text text="xmpp:${o.jid}?join"></converse-rich-text></p>
<p class="room-info"><strong>${i18n_desc}</strong>: <converse-rich-text text="${config.description}" render_styling></converse-rich-text></p>
${ (o.subject) ? subject(o) : '' }
<p class="room-info"><strong>${i18n_online_users}</strong>: ${num_occupants}</p>
<p class="room-info"><strong>${i18n_features}</strong>:
<div class="chatroom-features">
<ul class="features-list">
${ features.passwordprotected ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-lock"></converse-icon>${i18n_password_protected} - <em>${i18n_password_help}</em></li>` : '' }
${ features.unsecured ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-unlock"></converse-icon>${i18n_no_password_required} - <em>${i18n_no_pass_help}</em></li>` : '' }
${ features.hidden ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-eye-slash"></converse-icon>${i18n_hidden} - <em>${i18n_hidden_help}</em></li>` : '' }
${ features.public_room ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-eye"></converse-icon>${i18n_public} - <em>${o.__('This groupchat is publicly searchable') }</em></li>` : '' }
${ features.membersonly ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-address-book"></converse-icon>${i18n_members_only} - <em>${i18n_members_help}</em></li>` : '' }
${ features.open ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-globe"></converse-icon>${i18n_open} - <em>${i18n_open_help}</em></li>` : '' }
${ features.persistent ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-save"></converse-icon>${i18n_persistent} - <em>${i18n_persistent_help}</em></li>` : '' }
${ features.temporary ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-snowflake-o"></converse-icon>${i18n_temporary} - <em>${i18n_temporary_help}</em></li>` : '' }
${ features.nonanonymous ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-id-card"></converse-icon>${i18n_not_anonymous} - <em>${i18n_not_anonymous_help}</em></li>` : '' }
${ features.semianonymous ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-user-secret"></converse-icon>${i18n_semi_anon} - <em>${i18n_semi_anon_help}</em></li>` : '' }
${ features.moderated ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-gavel"></converse-icon>${i18n_moderated} - <em>${i18n_moderated_help}</em></li>` : '' }
${ features.unmoderated ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-info-circle"></converse-icon>${i18n_not_moderated} - <em>${i18n_not_moderated_help}</em></li>` : '' }
${ features.mam_enabled ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-database"></converse-icon>${i18n_archiving} - <em>${i18n_archiving_help}</em></li>` : '' }
</ul>
</div>
</p>
</div>
</div>
<div class="modal-footer">${modal_close_button}</div>
</div>
</div>
</p>
`;
}

View File

@ -1,49 +1,35 @@
import { html } from "lit";
import { __ } from 'i18n';
import { modal_header_close_button } from "plugins/modal/templates/buttons.js"
export default (o) => {
export default (el) => {
const i18n_invite = __('Invite');
const i18n_invite_heading = __('Invite someone to this groupchat');
const i18n_jid_placeholder = __('user@example.org');
const i18n_error_message = __('Please enter a valid XMPP address');
const i18n_invite_label = __('XMPP Address');
const i18n_reason = __('Optional reason for the invitation');
return 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_invite_heading}</h5>
${modal_header_close_button}
</div>
<div class="modal-body">
<span class="modal-alert"></span>
<div class="suggestion-box room-invite">
<form @submit=${o.submitInviteForm}>
<div class="form-group">
<label class="clearfix" for="invitee_jids">${i18n_invite_label}:</label>
${ o.invalid_invite_jid ? html`<div class="error error-feedback">${i18n_error_message}</div>` : '' }
<input class="form-control suggestion-box__input"
required="required"
name="invitee_jids"
id="invitee_jids"
placeholder="${i18n_jid_placeholder}"
type="text"/>
<span class="suggestion-box__additions visually-hidden" role="status" aria-live="assertive" aria-relevant="additions"></span>
<ul class="suggestion-box__results suggestion-box__results--below" hidden=""></ul>
</div>
<div class="form-group">
<label>${i18n_reason}:</label>
<textarea class="form-control" name="reason"></textarea>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">${i18n_invite}</button>
</div>
</form>
</div>
</div>
<form class="converse-form" @submit=${(ev) => el.submitInviteForm(ev)}>
<div class="form-group">
<label class="clearfix" for="invitee_jids">${i18n_invite_label}:</label>
${ el.model.get('invalid_invite_jid') ? html`<div class="error error-feedback">${i18n_error_message}</div>` : '' }
<converse-autocomplete
.getAutoCompleteList=${() => el.getAutoCompleteList()}
?autofocus=${true}
min_chars="1"
position="below"
required="required"
name="invitee_jids"
id="invitee_jids"
placeholder="${i18n_jid_placeholder}">
</converse-autocomplete>
</div>
</div>
<div class="form-group">
<label>${i18n_reason}:</label>
<textarea class="form-control" name="reason"></textarea>
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary" value="${i18n_invite}"/>
</div>
</form>
`;
}

View File

@ -1,21 +0,0 @@
import { __ } from 'i18n';
import { html } from "lit";
import { modal_header_close_button } from "plugins/modal/templates/buttons.js"
export default (modal) => {
const jid = modal.model.get('jid');
const i18n_heading = __('Change your nickname');
return 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_heading}</h5>
${modal_header_close_button}
</div>
<div class="modal-body d-flex flex-column">
<converse-muc-nickname-form jid="${jid}"></converse-muc-nickname-form>
</div>
</div>
</div>`;
}

View File

@ -1,53 +1,39 @@
import 'shared/avatar/avatar.js';
import { __ } from 'i18n';
import { html } from "lit";
import { modal_close_button, modal_header_close_button } from "plugins/modal/templates/buttons.js"
export default (o) => {
return 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" class="d-flex">
<div class="row">
<div class="col-auto">
<converse-avatar
class="avatar modal-avatar"
.data=${o.vcard?.attributes}
nonce=${o.vcard?.get('vcard_updated')}
height="120" width="120"></converse-avatar>
</div>
<div class="col">
<ul class="occupant-details">
<li>
${ o.nick ? html`<div class="row"><strong>${__('Nickname')}:</strong></div><div class="row">${o.nick}</div>` : '' }
</li>
<li>
${ o.jid ? html`<div class="row"><strong>${__('XMPP Address')}:</strong></div><div class="row">${o.jid}</div>` : '' }
</li>
<li>
${ o.affiliation ? html`<div class="row"><strong>${__('Affiliation')}:</strong></div><div class="row">${o.affiliation}</div>` : '' }
</li>
<li>
${ o.role ? html`<div class="row"><strong>${__('Roles')}:</strong></div><div class="row">${o.role}</div>` : '' }
</li>
<li>
${ o.hats ? html`<div class="row"><strong>${__('Hats')}:</strong></div><div class="row">${o.hats}</div>` : '' }
</li>
<li>
${ o.occupant_id ? html`<div class="row"><strong>${__('Occupant Id')}:</strong></div><div class="row">${o.occupant_id}</div>` : '' }
</li>
</ul>
</div>
</div>
</div>
<div class="modal-footer">
${modal_close_button}
</div>
<div class="row">
<div class="col-auto">
<converse-avatar
class="avatar modal-avatar"
.data=${o.vcard?.attributes}
nonce=${o.vcard?.get('vcard_updated')}
height="120" width="120"></converse-avatar>
</div>
<div class="col">
<ul class="occupant-details">
<li>
${ o.nick ? html`<div class="row"><strong>${__('Nickname')}:</strong></div><div class="row">${o.nick}</div>` : '' }
</li>
<li>
${ o.jid ? html`<div class="row"><strong>${__('XMPP Address')}:</strong></div><div class="row">${o.jid}</div>` : '' }
</li>
<li>
${ o.affiliation ? html`<div class="row"><strong>${__('Affiliation')}:</strong></div><div class="row">${o.affiliation}</div>` : '' }
</li>
<li>
${ o.role ? html`<div class="row"><strong>${__('Roles')}:</strong></div><div class="row">${o.role}</div>` : '' }
</li>
<li>
${ o.hats ? html`<div class="row"><strong>${__('Hats')}:</strong></div><div class="row">${o.hats}</div>` : '' }
</li>
<li>
${ o.occupant_id ? html`<div class="row"><strong>${__('Occupant Id')}:</strong></div><div class="row">${o.occupant_id}</div>` : '' }
</li>
</ul>
</div>
</div>
`;

View File

@ -25,6 +25,7 @@ export default class ModeratorTools extends CustomElement {
muc: { type: Object, attribute: false },
role: { type: String },
roles_filter: { type: String, attribute: false },
tab: { type: String },
users_with_affiliation: { type: Array, attribute: false },
users_with_role: { type: Array, attribute: false },
};
@ -32,6 +33,7 @@ export default class ModeratorTools extends CustomElement {
constructor () {
super();
this.tab = 'affiliations';
this.affiliation = '';
this.affiliations_filter = '';
this.role = '';
@ -72,6 +74,7 @@ export default class ModeratorTools extends CustomElement {
'queryable_roles': ROLES.filter(a => !api.settings.get('modtools_disable_query').includes(a)),
'roles_filter': this.roles_filter,
'switchTab': ev => this.switchTab(ev),
'tab': this.tab,
'toggleForm': ev => this.toggleForm(ev),
'users_with_affiliation': this.users_with_affiliation,
'users_with_role': this.users_with_role,
@ -81,6 +84,13 @@ export default class ModeratorTools extends CustomElement {
}
}
switchTab (ev) {
ev.stopPropagation();
ev.preventDefault();
this.tab = ev.target.getAttribute('data-name');
this.requestUpdate();
}
async onSearchAffiliationChange () {
if (!this.affiliation) {
return;

View File

@ -1,12 +1,14 @@
#add-chatroom-modal {
converse-autocomplete {
.suggestion-box__results--below {
height: 10em;
overflow: auto;
}
converse-add-muc-modal {
.add-chatroom {
converse-autocomplete {
.suggestion-box__results--below {
height: 10em;
overflow: auto;
}
.suggestion-box ul li {
display: block;
.suggestion-box ul li {
display: block;
}
}
}
}

View File

@ -5,7 +5,6 @@
@import "./controlbox.scss";
@import "./muc.scss";
@import "./muc-details-modal.scss";
converse-muc-disconnected,
converse-muc-destroyed {

View File

@ -1,8 +1,14 @@
#muc-details-modal {
converse-muc-details-modal {
.features-list {
margin-left: 1em;
}
.room-info {
strong {
color: var(--muc-color);
}
}
.chatroom-features {
width: 100%;
.features-list {
@ -13,9 +19,8 @@
padding-right: 0;
font-size: 1em;
cursor: help;
.fa {
converse-icon {
margin-right: 0.5em;
color: var(--text-color);
}
}
}

View File

@ -1,8 +0,0 @@
#muc-details-modal {
.room-info {
strong {
color: var(--muc-color);
}
}
}

View File

@ -1,56 +0,0 @@
import DOMPurify from 'dompurify';
import { __ } from 'i18n';
import { api } from '@converse/headless/core.js';
import { html } from "lit";
import { modal_header_close_button } from "plugins/modal/templates/buttons.js"
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { getAutoCompleteList } from "../search.js";
const nickname_input = (o) => {
const i18n_nickname = __('Nickname');
const i18n_required_field = __('This field is required');
return 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) => {
const i18n_join = __('Join');
const i18n_enter = __('Enter a new Groupchat');
return 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>` : '' }
<converse-autocomplete
.getAutoCompleteList=${getAutoCompleteList}
?autofocus=${true}
min_chars="3"
position="below"
placeholder="${o.chatroom_placeholder}"
class="add-muc-autocomplete"
name="chatroom">
</converse-autocomplete>
</div>
${ o.muc_roomid_policy_hint ? html`<div class="form-group">${unsafeHTML(DOMPurify.sanitize(o.muc_roomid_policy_hint, {'ALLOWED_TAGS': ['b', 'br', 'em']}))}</div>` : '' }
${ !api.settings.get('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

@ -146,13 +146,25 @@ const affiliation_list_item = (o) => html`
`;
const tpl_navigation = () => html`
const tpl_navigation = (o) => html`
<ul class="nav nav-pills justify-content-center">
<li role="presentation" class="nav-item">
<a class="nav-link active" id="affiliations-tab" href="#affiliations-tabpanel" aria-controls="affiliations-tabpanel" role="tab" data-toggle="tab">Affiliations</a>
<a class="nav-link ${o.tab === "affiliations" ? "active" : ""}"
id="affiliations-tab"
href="#affiliations-tabpanel"
aria-controls="affiliations-tabpanel"
role="tab"
data-name="affiliations"
@click=${o.switchTab}>Affiliations</a>
</li>
<li role="presentation" class="nav-item">
<a class="nav-link" id="roles-tab" href="#roles-tabpanel" aria-controls="roles-tabpanel" role="tab" data-toggle="tab">Roles</a>
<a class="nav-link ${o.tab === "roles" ? "active" : ""}"
id="roles-tab"
href="#roles-tabpanel"
aria-controls="roles-tabpanel"
role="tab"
data-name="roles"
@click=${o.switchTab}>Roles</a>
</li>
</ul>
`;
@ -178,12 +190,12 @@ export default (o) => {
const show_both_tabs = o.queryable_roles.length && o.queryable_affiliations.length;
return html`
${o.alert_message ? html`<div class="alert alert-${o.alert_type}" role="alert">${o.alert_message}</div>` : '' }
${ show_both_tabs ? tpl_navigation() : '' }
${ show_both_tabs ? tpl_navigation(o) : '' }
<div class="tab-content">
${ o.queryable_affiliations.length ? html`
<div class="tab-pane tab-pane--columns ${ o.queryable_affiliations.length ? 'active' : ''}" id="affiliations-tabpanel" role="tabpanel" aria-labelledby="affiliations-tab">
<div class="tab-pane tab-pane--columns ${ o.tab === 'affiliations' ? 'active' : ''}" id="affiliations-tabpanel" role="tabpanel" aria-labelledby="affiliations-tab">
<form class="converse-form query-affiliation" @submit=${o.queryAffiliation}>
<p class="helptext pb-3">${i18n_helptext_affiliation}</p>
<div class="form-group">
@ -225,7 +237,7 @@ export default (o) => {
</div>` : '' }
${ o.queryable_roles.length ? html`
<div class="tab-pane tab-pane--columns ${ !show_both_tabs && o.queryable_roles.length ? 'active' : ''}" id="roles-tabpanel" role="tabpanel" aria-labelledby="roles-tab">
<div class="tab-pane tab-pane--columns ${ o.tab === 'roles' ? 'active' : ''}" id="roles-tabpanel" role="tabpanel" aria-labelledby="roles-tab">
<form class="converse-form query-role" @submit=${o.queryRole}>
<p class="helptext pb-3">${i18n_helptext_role}</p>
<div class="form-group">

View File

@ -1,7 +1,6 @@
import { __ } from 'i18n';
import { html } from "lit";
import { repeat } from 'lit/directives/repeat.js';
import { modal_close_button, modal_header_close_button } from "plugins/modal/templates/buttons.js"
import spinner from "templates/spinner.js";
@ -14,6 +13,7 @@ const form = (o) => {
<div class="form-group">
<label for="chatroom">${i18n_server_address}:</label>
<input type="text"
autofocus
@change=${o.setDomainFromEvent}
value="${o.muc_domain || ''}"
required="required"
@ -34,16 +34,16 @@ const tpl_item = (o, item) => {
<li class="room-item list-group-item">
<div class="available-chatroom d-flex flex-row">
<a class="open-room available-room w-100"
@click=${o.openRoom}
data-room-jid="${item.jid}"
data-room-name="${item.name}"
title="${i18n_open_title}"
href="#">${item.name || item.jid}</a>
<a class="right room-info icon-room-info"
@click=${o.toggleRoomInfo}
data-room-jid="${item.jid}"
title="${i18n_info_title}"
href="#"></a>
@click=${o.openRoom}
data-room-jid="${item.jid}"
data-room-name="${item.name}"
title="${i18n_open_title}"
href="#">${item.name || item.jid}</a>
<a class="right room-info icon-room-info"
@click=${o.toggleRoomInfo}
data-room-jid="${item.jid}"
title="${i18n_info_title}"
href="#"></a>
</div>
</li>
`;
@ -51,25 +51,12 @@ const tpl_item = (o, item) => {
export default (o) => {
const i18n_list_chatrooms = __('Query for Groupchats');
return html`
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="muc-list-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">
${ o.loading_items ? html`<li class="list-group-item"> ${spinner()} </li>` : '' }
${ o.feedback_text ? html`<li class="list-group-item active">${ o.feedback_text }</li>` : '' }
${repeat(o.items, item => item.jid, item => tpl_item(o, item))}
</ul>
</div>
<div class="modal-footer">${modal_close_button}</div>
</div>
</div>
${o.show_form ? form(o) : '' }
<ul class="available-chatrooms list-group">
${ o.loading_items ? html`<li class="list-group-item"> ${spinner()} </li>` : '' }
${ o.feedback_text ? html`<li class="list-group-item active">${ o.feedback_text }</li>` : '' }
${repeat(o.items, item => item.jid, item => tpl_item(o, item))}
</ul>
`;
}

View File

@ -12,9 +12,9 @@ export default (el) => {
const validation_message = el.model?.get('nickname_validation_message');
return html`
<div class="chatroom-form-container muc-nickname-form"
@submit=${ev => el.submitNickname(ev)}>
<form class="converse-form chatroom-form converse-centered-form">
<div class="chatroom-form-container muc-nickname-form">
<form class="converse-form chatroom-form converse-centered-form"
@submit=${ev => el.submitNickname(ev)}>
<fieldset class="form-group">
<label>${i18n_heading}</label>
<p class="validation-message">${validation_message}</p>
@ -26,7 +26,10 @@ export default (el) => {
placeholder="${i18n_nickname}"/>
</fieldset>
<fieldset class="form-group">
<input type="submit" class="btn btn-primary" name="join" value="${i18n_join}"/>
<input type="submit"
class="btn btn-primary"
name="join"
value="${i18n_join}"/>
</fieldset>
</form>
</div>`;

View File

@ -60,9 +60,9 @@ describe("A Groupchat Message", function () {
expect(view.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
const edit = await u.waitUntil(() => view.querySelector('.chat-msg__content .fa-edit'));
edit.click();
const modal = _converse.api.modal.get('message-versions-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
const older_msgs = modal.el.querySelectorAll('.older-msg');
const modal = _converse.api.modal.get('converse-message-versions-modal');
await u.waitUntil(() => u.isVisible(modal), 1000);
const older_msgs = modal.querySelectorAll('.older-msg');
expect(older_msgs.length).toBe(2);
expect(older_msgs[0].textContent.includes('But soft, what light through yonder airlock breaks?')).toBe(true);
expect(older_msgs[1].textContent.includes('But soft, what light through yonder chimney breaks?')).toBe(true);
@ -151,9 +151,9 @@ describe("A Groupchat Message", function () {
expect(view.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
const edit = await u.waitUntil(() => view.querySelector('.chat-msg__content .fa-edit'));
edit.click();
const modal = _converse.api.modal.get('message-versions-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
const older_msgs = modal.el.querySelectorAll('.older-msg');
const modal = _converse.api.modal.get('converse-message-versions-modal');
await u.waitUntil(() => u.isVisible(modal), 1000);
const older_msgs = modal.querySelectorAll('.older-msg');
expect(older_msgs.length).toBe(2);
expect(older_msgs[0].textContent.includes('But soft, what light through yonder airlock breaks?')).toBe(true);
expect(older_msgs[1].textContent.includes('But soft, what light through yonder chimney breaks?')).toBe(true);

View File

@ -14,7 +14,7 @@ async function openModtools (_converse, view) {
const message_form = view.querySelector('converse-muc-message-form');
message_form.onKeyDown(enter);
const modal = await u.waitUntil(() => _converse.api.modal.get('converse-modtools-modal'));
await u.waitUntil(() => u.isVisible(modal.el), 1000);
await u.waitUntil(() => u.isVisible(modal), 1000);
return modal;
}
@ -37,18 +37,18 @@ describe("The groupchat moderator tool", function () {
await u.waitUntil(() => (view.model.occupants.length === 5), 1000);
const modal = await openModtools(_converse, view);
let tab = modal.el.querySelector('#affiliations-tab');
let tab = modal.querySelector('#affiliations-tab');
// Clear so that we don't match older stanzas
_converse.connection.IQ_stanzas = [];
tab.click();
let select = modal.el.querySelector('.select-affiliation');
let select = modal.querySelector('.select-affiliation');
expect(select.value).toBe('owner');
select.value = 'admin';
let button = modal.el.querySelector('.btn-primary[name="users_with_affiliation"]');
let button = modal.querySelector('.btn-primary[name="users_with_affiliation"]');
button.click();
await u.waitUntil(() => !modal.loading_users_with_affiliation);
await u.waitUntil(() => modal.el.querySelectorAll('.list-group--users > li').length);
let user_els = modal.el.querySelectorAll('.list-group--users > li');
await u.waitUntil(() => modal.querySelectorAll('.list-group--users > li').length);
let user_els = modal.querySelectorAll('.list-group--users > li');
expect(user_els.length).toBe(1);
expect(user_els[0].querySelector('.list-group-item.active').textContent.trim()).toBe('JID: wiccarocks@shakespeare.lit');
expect(user_els[0].querySelector('.list-group-item:nth-child(2n)').textContent.trim()).toBe('Nickname: wiccan');
@ -58,8 +58,8 @@ describe("The groupchat moderator tool", function () {
select.value = 'owner';
button.click();
await u.waitUntil(() => !modal.loading_users_with_affiliation);
await u.waitUntil(() => modal.el.querySelectorAll('.list-group--users > li').length === 2);
user_els = modal.el.querySelectorAll('.list-group--users > li');
await u.waitUntil(() => modal.querySelectorAll('.list-group--users > li').length === 2);
user_els = modal.querySelectorAll('.list-group--users > li');
expect(user_els.length).toBe(2);
expect(user_els[0].querySelector('.list-group-item.active').textContent.trim()).toBe('JID: romeo@montague.lit');
expect(user_els[0].querySelector('.list-group-item:nth-child(2n)').textContent.trim()).toBe('Nickname: romeo');
@ -112,25 +112,25 @@ describe("The groupchat moderator tool", function () {
];
await mock.returnMemberLists(_converse, muc_jid, members);
await u.waitUntil(() => view.model.occupants.pluck('affiliation').filter(o => o === 'owner').length === 1);
const alert = modal.el.querySelector('.alert-primary');
const alert = modal.querySelector('.alert-primary');
expect(alert.textContent.trim()).toBe('Affiliation changed');
await u.waitUntil(() => modal.el.querySelectorAll('.list-group--users > li').length === 1);
user_els = modal.el.querySelectorAll('.list-group--users > li');
await u.waitUntil(() => modal.querySelectorAll('.list-group--users > li').length === 1);
user_els = modal.querySelectorAll('.list-group--users > li');
expect(user_els.length).toBe(1);
expect(user_els[0].querySelector('.list-group-item.active').textContent.trim()).toBe('JID: romeo@montague.lit');
expect(user_els[0].querySelector('.list-group-item:nth-child(2n)').textContent.trim()).toBe('Nickname: romeo');
expect(user_els[0].querySelector('.list-group-item:nth-child(3n) div').textContent.trim()).toBe('Affiliation: owner');
tab = modal.el.querySelector('#roles-tab');
tab.click();
select = modal.el.querySelector('.select-role');
expect(u.isVisible(select)).toBe(true);
modal.querySelector('#roles-tab').click();
select = modal.querySelector('.select-role');
await u.waitUntil(() => u.isVisible(select));
expect(select.value).toBe('moderator');
button = modal.el.querySelector('.btn-primary[name="users_with_role"]');
button = modal.querySelector('.btn-primary[name="users_with_role"]');
button.click();
const roles_panel = modal.el.querySelector('#roles-tabpanel');
const roles_panel = modal.querySelector('#roles-tabpanel');
await u.waitUntil(() => roles_panel.querySelectorAll('.list-group--users > li').length === 1);
select.value = 'participant';
button.click();
@ -158,35 +158,35 @@ describe("The groupchat moderator tool", function () {
// Clear so that we don't match older stanzas
_converse.connection.IQ_stanzas = [];
const modal = await openModtools(_converse, view);
const select = modal.el.querySelector('.select-affiliation');
const select = modal.querySelector('.select-affiliation');
expect(select.value).toBe('owner');
select.value = 'member';
const button = modal.el.querySelector('.btn-primary[name="users_with_affiliation"]');
const button = modal.querySelector('.btn-primary[name="users_with_affiliation"]');
button.click();
await u.waitUntil(() => !modal.loading_users_with_affiliation);
await u.waitUntil(() => modal.el.querySelectorAll('.list-group--users > li').length === 6);
await u.waitUntil(() => modal.querySelectorAll('.list-group--users > li').length === 6);
const nicks = Array.from(modal.el.querySelectorAll('.list-group--users > li')).map(el => el.getAttribute('data-nick'));
const nicks = Array.from(modal.querySelectorAll('.list-group--users > li')).map(el => el.getAttribute('data-nick'));
expect(nicks.join(' ')).toBe('gower juliet romeo thirdwitch wiccan witch');
const filter = modal.el.querySelector('[name="filter"]');
const filter = modal.querySelector('[name="filter"]');
expect(filter).not.toBe(null);
filter.value = 'romeo';
u.triggerEvent(filter, "keyup", "KeyboardEvent");
await u.waitUntil(() => ( modal.el.querySelectorAll('.list-group--users > li').length === 1));
await u.waitUntil(() => ( modal.querySelectorAll('.list-group--users > li').length === 1));
filter.value = 'r';
u.triggerEvent(filter, "keyup", "KeyboardEvent");
await u.waitUntil(() => ( modal.el.querySelectorAll('.list-group--users > li').length === 3));
await u.waitUntil(() => ( modal.querySelectorAll('.list-group--users > li').length === 3));
filter.value = 'gower';
u.triggerEvent(filter, "keyup", "KeyboardEvent");
await u.waitUntil(() => ( modal.el.querySelectorAll('.list-group--users > li').length === 1));
await u.waitUntil(() => ( modal.querySelectorAll('.list-group--users > li').length === 1));
filter.value = 'RoMeO';
u.triggerEvent(filter, "keyup", "KeyboardEvent");
await u.waitUntil(() => ( modal.el.querySelectorAll('.list-group--users > li').length === 1));
await u.waitUntil(() => ( modal.querySelectorAll('.list-group--users > li').length === 1));
}));
@ -259,40 +259,40 @@ describe("The groupchat moderator tool", function () {
message_form.onKeyDown(enter);
const modal = await u.waitUntil(() => _converse.api.modal.get('converse-modtools-modal'));
await u.waitUntil(() => u.isVisible(modal.el), 1000);
await u.waitUntil(() => u.isVisible(modal), 1000);
const tab = modal.el.querySelector('#roles-tab');
const tab = modal.querySelector('#roles-tab');
tab.click();
// Clear so that we don't match older stanzas
_converse.connection.IQ_stanzas = [];
const select = modal.el.querySelector('.select-role');
const select = modal.querySelector('.select-role');
expect(select.value).toBe('moderator');
select.value = 'participant';
const button = modal.el.querySelector('.btn-primary[name="users_with_role"]');
const button = modal.querySelector('.btn-primary[name="users_with_role"]');
button.click();
await u.waitUntil(() => !modal.loading_users_with_role);
await u.waitUntil(() => modal.el.querySelectorAll('.list-group--users > li').length === 6);
await u.waitUntil(() => modal.querySelectorAll('.list-group--users > li').length === 6);
const nicks = Array.from(modal.el.querySelectorAll('.list-group--users > li')).map(el => el.getAttribute('data-nick'));
const nicks = Array.from(modal.querySelectorAll('.list-group--users > li')).map(el => el.getAttribute('data-nick'));
expect(nicks.join(' ')).toBe('crone newb nomorenicks oldhag some1 tux');
const filter = modal.el.querySelector('[name="filter"]');
const filter = modal.querySelector('[name="filter"]');
expect(filter).not.toBe(null);
filter.value = 'tux';
u.triggerEvent(filter, "keyup", "KeyboardEvent");
await u.waitUntil(() => ( modal.el.querySelectorAll('.list-group--users > li').length === 1));
await u.waitUntil(() => ( modal.querySelectorAll('.list-group--users > li').length === 1));
filter.value = 'r';
u.triggerEvent(filter, "keyup", "KeyboardEvent");
await u.waitUntil(() => ( modal.el.querySelectorAll('.list-group--users > li').length === 2));
await u.waitUntil(() => ( modal.querySelectorAll('.list-group--users > li').length === 2));
filter.value = 'crone';
u.triggerEvent(filter, "keyup", "KeyboardEvent");
await u.waitUntil(() => ( modal.el.querySelectorAll('.list-group--users > li').length === 1));
await u.waitUntil(() => ( modal.querySelectorAll('.list-group--users > li').length === 1));
}));
it("shows an error message if a particular affiliation list may not be retrieved",
@ -310,14 +310,14 @@ describe("The groupchat moderator tool", function () {
const view = _converse.chatboxviews.get(muc_jid);
await u.waitUntil(() => (view.model.occupants.length === 5));
const modal = await openModtools(_converse, view);
const tab = modal.el.querySelector('#affiliations-tab');
const tab = modal.querySelector('#affiliations-tab');
// Clear so that we don't match older stanzas
_converse.connection.IQ_stanzas = [];
const IQ_stanzas = _converse.connection.IQ_stanzas;
tab.click();
const select = modal.el.querySelector('.select-affiliation');
const select = modal.querySelector('.select-affiliation');
select.value = 'outcast';
const button = modal.el.querySelector('.btn-primary[name="users_with_affiliation"]');
const button = modal.querySelector('.btn-primary[name="users_with_affiliation"]');
button.click();
const iq_query = await u.waitUntil(() => _.filter(
@ -338,10 +338,10 @@ describe("The groupchat moderator tool", function () {
_converse.connection._dataRecv(mock.createRequest(error));
await u.waitUntil(() => !modal.loading_users_with_affiliation);
const alert = await u.waitUntil(() => modal.el.querySelector('.alert'));
const alert = await u.waitUntil(() => modal.querySelector('.alert'));
expect(alert.textContent.trim()).toBe('Error: not allowed to fetch outcast list for MUC lounge@montague.lit');
const user_els = modal.el.querySelectorAll('.list-group--users > li');
const user_els = modal.querySelectorAll('.list-group--users > li');
expect(user_els.length).toBe(1);
expect(user_els[0].textContent.trim()).toBe('No users with that affiliation found.');
}));
@ -361,16 +361,16 @@ describe("The groupchat moderator tool", function () {
// Clear so that we don't match older stanzas
_converse.connection.IQ_stanzas = [];
const tab = modal.el.querySelector('#affiliations-tab');
const tab = modal.querySelector('#affiliations-tab');
tab.click();
const select = modal.el.querySelector('.select-affiliation');
const select = modal.querySelector('.select-affiliation');
select.value = 'member';
const button = modal.el.querySelector('.btn-primary[name="users_with_affiliation"]');
const button = modal.querySelector('.btn-primary[name="users_with_affiliation"]');
button.click();
await u.waitUntil(() => !modal.loading_users_with_affiliation);
await u.waitUntil(() => modal.el.querySelectorAll('.list-group--users > li').length === 1);
await u.waitUntil(() => modal.querySelectorAll('.list-group--users > li').length === 1);
const user_els = modal.el.querySelectorAll('.list-group--users > li');
const user_els = modal.querySelectorAll('.list-group--users > li');
const toggle = user_els[0].querySelector('.list-group-item:nth-child(3n) .toggle-form');
const form = user_els[0].querySelector('.list-group-item:nth-child(3n) .affiliation-form');
expect(u.hasClass('hidden', form)).toBeTruthy();
@ -422,19 +422,19 @@ describe("The groupchat moderator tool", function () {
const view = _converse.chatboxviews.get(muc_jid);
await u.waitUntil(() => (view.model.occupants.length === 3));
const modal = await openModtools(_converse, view);
const tab = modal.el.querySelector('#affiliations-tab');
const tab = modal.querySelector('#affiliations-tab');
// Clear so that we don't match older stanzas
_converse.connection.IQ_stanzas = [];
tab.click();
const show_affiliation_dropdown = modal.el.querySelector('.select-affiliation');
const show_affiliation_dropdown = modal.querySelector('.select-affiliation');
show_affiliation_dropdown.value = 'member';
const button = modal.el.querySelector('.btn-primary[name="users_with_affiliation"]');
const button = modal.querySelector('.btn-primary[name="users_with_affiliation"]');
button.click();
await u.waitUntil(() => !modal.loading_users_with_affiliation);
await u.waitUntil(() => modal.el.querySelectorAll('.list-group--users > li').length === 2);
await u.waitUntil(() => modal.querySelectorAll('.list-group--users > li').length === 2);
const user_els = modal.el.querySelectorAll('.list-group--users > li');
const user_els = modal.querySelectorAll('.list-group--users > li');
let change_affiliation_dropdown = user_els[0].querySelector('.select-affiliation');
expect(Array.from(change_affiliation_dropdown.options).map(o => o.value)).toEqual(['member', 'outcast', 'none']);

View File

@ -12,32 +12,32 @@ describe('The "Groupchats" Add modal', function () {
const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
roomspanel.querySelector('.show-add-muc-modal').click();
mock.closeControlBox(_converse);
const modal = _converse.api.modal.get('add-chatroom-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
const modal = _converse.api.modal.get('converse-add-muc-modal');
await u.waitUntil(() => u.isVisible(modal), 1000);
let label_name = modal.el.querySelector('label[for="chatroom"]');
let label_name = modal.querySelector('label[for="chatroom"]');
expect(label_name.textContent.trim()).toBe('Groupchat address:');
let name_input = modal.el.querySelector('input[name="chatroom"]');
const name_input = modal.querySelector('input[name="chatroom"]');
expect(name_input.placeholder).toBe('name@conference.example.org');
const label_nick = modal.el.querySelector('label[for="nickname"]');
const label_nick = modal.querySelector('label[for="nickname"]');
expect(label_nick.textContent.trim()).toBe('Nickname:');
const nick_input = modal.el.querySelector('input[name="nickname"]');
const nick_input = modal.querySelector('input[name="nickname"]');
expect(nick_input.value).toBe('');
nick_input.value = 'romeo';
expect(modal.el.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat');
expect(modal.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat');
spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
modal.el.querySelector('input[name="chatroom"]').value = 'lounge@muc.montague.lit';
modal.el.querySelector('form input[type="submit"]').click();
modal.querySelector('input[name="chatroom"]').value = 'lounge@muc.montague.lit';
modal.querySelector('form input[type="submit"]').click();
await u.waitUntil(() => _converse.chatboxes.length);
await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 1);
roomspanel.model.set('muc_domain', 'muc.example.org');
roomspanel.querySelector('.show-add-muc-modal').click();
label_name = modal.el.querySelector('label[for="chatroom"]');
expect(label_name.textContent.trim()).toBe('Groupchat address:');
await u.waitUntil(() => modal.el.querySelector('input[name="chatroom"]')?.placeholder === 'name@muc.example.org');
label_name = modal.querySelector('label[for="chatroom"]');
expect(label_name.textContent.trim()).toBe('Groupchat name:');
await u.waitUntil(() => modal.querySelector('input[name="chatroom"]')?.placeholder === 'name@muc.example.org');
})
);
@ -46,31 +46,31 @@ describe('The "Groupchats" Add modal', function () {
await mock.openControlBox(_converse);
const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
roomspanel.querySelector('.show-add-muc-modal').click();
const modal = _converse.api.modal.get('add-chatroom-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
expect(modal.el.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat');
const modal = _converse.api.modal.get('converse-add-muc-modal');
await u.waitUntil(() => u.isVisible(modal), 1000);
expect(modal.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat');
spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
const label_name = modal.el.querySelector('label[for="chatroom"]');
const label_name = modal.querySelector('label[for="chatroom"]');
expect(label_name.textContent.trim()).toBe('Groupchat name:');
let name_input = modal.el.querySelector('input[name="chatroom"]');
let name_input = modal.querySelector('input[name="chatroom"]');
expect(name_input.placeholder).toBe('name@muc.example.org');
name_input.value = 'lounge';
let nick_input = modal.el.querySelector('input[name="nickname"]');
let nick_input = modal.querySelector('input[name="nickname"]');
nick_input.value = 'max';
modal.el.querySelector('form input[type="submit"]').click();
modal.querySelector('form input[type="submit"]').click();
await u.waitUntil(() => _converse.chatboxes.length);
await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 1);
expect(_converse.chatboxes.models.map(m => m.get('id')).includes('lounge@muc.example.org')).toBe(true);
// However, you can still open MUCs with different domains
roomspanel.querySelector('.show-add-muc-modal').click();
await u.waitUntil(() => u.isVisible(modal.el), 1000);
name_input = modal.el.querySelector('input[name="chatroom"]');
await u.waitUntil(() => u.isVisible(modal), 1000);
name_input = modal.querySelector('input[name="chatroom"]');
name_input.value = 'lounge@conference.example.org';
nick_input = modal.el.querySelector('input[name="nickname"]');
nick_input = modal.querySelector('input[name="nickname"]');
nick_input.value = 'max';
modal.el.querySelector('form input[type="submit"]').click();
modal.querySelector('form input[type="submit"]').click();
await u.waitUntil(() => _converse.chatboxes.models.filter(c => c.get('type') === 'chatroom').length === 2);
await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 2);
expect(_converse.chatboxes.models.map(m => m.get('id')).includes('lounge@conference.example.org')).toBe(
@ -87,30 +87,30 @@ describe('The "Groupchats" Add modal', function () {
await mock.openControlBox(_converse);
const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
roomspanel.querySelector('.show-add-muc-modal').click();
const modal = _converse.api.modal.get('add-chatroom-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
expect(modal.el.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat');
const modal = _converse.api.modal.get('converse-add-muc-modal');
await u.waitUntil(() => u.isVisible(modal), 1000);
expect(modal.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat');
spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
const label_name = modal.el.querySelector('label[for="chatroom"]');
const label_name = modal.querySelector('label[for="chatroom"]');
expect(label_name.textContent.trim()).toBe('Groupchat name:');
let name_input = modal.el.querySelector('input[name="chatroom"]');
let name_input = modal.querySelector('input[name="chatroom"]');
expect(name_input.placeholder).toBe('');
name_input.value = 'lounge';
let nick_input = modal.el.querySelector('input[name="nickname"]');
let nick_input = modal.querySelector('input[name="nickname"]');
nick_input.value = 'max';
modal.el.querySelector('form input[type="submit"]').click();
modal.querySelector('form input[type="submit"]').click();
await u.waitUntil(() => _converse.chatboxes.length);
await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 1);
expect(_converse.chatboxes.models.map(m => m.get('id')).includes('lounge@muc.example.org')).toBe(true);
// However, you can still open MUCs with different domains
roomspanel.querySelector('.show-add-muc-modal').click();
await u.waitUntil(() => u.isVisible(modal.el), 1000);
name_input = modal.el.querySelector('input[name="chatroom"]');
await u.waitUntil(() => u.isVisible(modal), 1000);
name_input = modal.querySelector('input[name="chatroom"]');
name_input.value = 'lounge@conference';
nick_input = modal.el.querySelector('input[name="nickname"]');
nick_input = modal.querySelector('input[name="nickname"]');
nick_input.value = 'max';
modal.el.querySelector('form input[type="submit"]').click();
modal.querySelector('form input[type="submit"]').click();
await u.waitUntil(
() => _converse.chatboxes.models.filter(c => c.get('type') === 'chatroom').length === 2
);

View File

@ -10,17 +10,17 @@ describe('The "Groupchats" List modal', function () {
const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
roomspanel.querySelector('.show-list-muc-modal').click();
mock.closeControlBox(_converse);
const modal = _converse.api.modal.get('muc-list-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
const modal = _converse.api.modal.get('converse-muc-list-modal');
await u.waitUntil(() => u.isVisible(modal), 1000);
spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
// See: https://xmpp.org/extensions/xep-0045.html#disco-rooms
expect(modal.el.querySelectorAll('.available-chatrooms li').length).toBe(0);
expect(modal.querySelectorAll('.available-chatrooms li').length).toBe(0);
const server_input = modal.el.querySelector('input[name="server"]');
const server_input = modal.querySelector('input[name="server"]');
expect(server_input.placeholder).toBe('conference.example.org');
server_input.value = 'chat.shakespeare.lit';
modal.el.querySelector('input[type="submit"]').click();
modal.querySelector('input[type="submit"]').click();
await u.waitUntil(() => _converse.chatboxes.length);
const IQ_stanzas = _converse.connection.IQ_stanzas;
@ -55,8 +55,8 @@ describe('The "Groupchats" List modal', function () {
.c('item', { jid: 'street@chat.shakespeare.lit', name: 'A street' }).nodeTree;
_converse.connection._dataRecv(mock.createRequest(iq));
await u.waitUntil(() => modal.el.querySelectorAll('.available-chatrooms li').length === 11);
const rooms = modal.el.querySelectorAll('.available-chatrooms li');
await u.waitUntil(() => modal.querySelectorAll('.available-chatrooms li').length === 11);
const rooms = modal.querySelectorAll('.available-chatrooms li');
expect(rooms[0].textContent.trim()).toBe('Groupchats found');
expect(rooms[1].textContent.trim()).toBe('A Lonely Heath');
expect(rooms[2].textContent.trim()).toBe('A Dark Cave');
@ -83,9 +83,9 @@ describe('The "Groupchats" List modal', function () {
const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
roomspanel.querySelector('.show-list-muc-modal').click();
mock.closeControlBox(_converse);
const modal = _converse.api.modal.get('muc-list-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
const server_input = modal.el.querySelector('input[name="server"]');
const modal = _converse.api.modal.get('converse-muc-list-modal');
await u.waitUntil(() => u.isVisible(modal), 1000);
const server_input = modal.querySelector('input[name="server"]');
expect(server_input.value).toBe('muc.example.org');
})
);
@ -99,12 +99,12 @@ describe('The "Groupchats" List modal', function () {
const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
roomspanel.querySelector('.show-list-muc-modal').click();
mock.closeControlBox(_converse);
const modal = _converse.api.modal.get('muc-list-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
const modal = _converse.api.modal.get('converse-muc-list-modal');
await u.waitUntil(() => u.isVisible(modal), 1000);
spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
expect(modal.el.querySelector('input[name="server"]')).toBe(null);
expect(modal.el.querySelector('input[type="submit"]')).toBe(null);
expect(modal.querySelector('input[name="server"]')).toBe(null);
expect(modal.querySelector('input[type="submit"]')).toBe(null);
await u.waitUntil(() => _converse.chatboxes.length);
const sent_stanza = await u.waitUntil(() =>
_converse.connection.sent_stanzas
@ -129,8 +129,8 @@ describe('The "Groupchats" List modal', function () {
.c('item', { jid: 'forres@chat.shakespeare.lit', name: 'The Palace' }).up();
_converse.connection._dataRecv(mock.createRequest(iq));
await u.waitUntil(() => modal.el.querySelectorAll('.available-chatrooms li').length === 4);
const rooms = modal.el.querySelectorAll('.available-chatrooms li');
await u.waitUntil(() => modal.querySelectorAll('.available-chatrooms li').length === 4);
const rooms = modal.querySelectorAll('.available-chatrooms li');
expect(rooms[0].textContent.trim()).toBe('Groupchats found');
expect(rooms[1].textContent.trim()).toBe('A Lonely Heath');
expect(rooms[2].textContent.trim()).toBe('A Dark Cave');

View File

@ -1332,21 +1332,21 @@ describe("Groupchats", function () {
await u.waitUntil(() => view.querySelector('.open-invite-modal'));
view.querySelector('.open-invite-modal').click();
const modal = _converse.api.modal.get('muc-invite-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000)
const modal = _converse.api.modal.get('converse-muc-invite-modal');
await u.waitUntil(() => u.isVisible(modal), 1000)
expect(modal.el.querySelectorAll('#invitee_jids').length).toBe(1);
expect(modal.el.querySelectorAll('textarea').length).toBe(1);
expect(modal.querySelectorAll('#invitee_jids').length).toBe(1);
expect(modal.querySelectorAll('textarea').length).toBe(1);
spyOn(view.model, 'directInvite').and.callThrough();
const input = modal.el.querySelector('#invitee_jids');
const input = modal.querySelector('#invitee_jids input');
input.value = "Balt";
modal.el.querySelector('button[type="submit"]').click();
modal.querySelector('input[type="submit"]').click();
await u.waitUntil(() => modal.el.querySelector('.error'));
await u.waitUntil(() => modal.querySelector('.error'));
const error = modal.el.querySelector('.error');
const error = modal.querySelector('.error');
expect(error.textContent).toBe('Please enter a valid XMPP address');
let evt = new Event('input');
@ -1354,7 +1354,7 @@ describe("Groupchats", function () {
let sent_stanza;
spyOn(_converse.connection, 'send').and.callFake(stanza => (sent_stanza = stanza));
const hint = await u.waitUntil(() => modal.el.querySelector('.suggestion-box__results li'));
const hint = await u.waitUntil(() => modal.querySelector('.suggestion-box__results li'));
expect(input.value).toBe('Balt');
expect(hint.textContent.trim()).toBe('Balthasar');
@ -1362,9 +1362,9 @@ describe("Groupchats", function () {
evt.button = 0;
hint.dispatchEvent(evt);
const textarea = modal.el.querySelector('textarea');
const textarea = modal.querySelector('textarea');
textarea.value = "Please join!";
modal.el.querySelector('button[type="submit"]').click();
modal.querySelector('input[type="submit"]').click();
expect(view.model.directInvite).toHaveBeenCalled();
expect(Strophe.serialize(sent_stanza)).toBe(
@ -1634,10 +1634,10 @@ describe("Groupchats", function () {
const info_el = view.querySelector(".show-muc-details-modal");
info_el.click();
let modal = _converse.api.modal.get('muc-details-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
let modal = _converse.api.modal.get('converse-muc-details-modal');
await u.waitUntil(() => u.isVisible(modal), 1000);
let features_list = modal.el.querySelector('.features-list');
let features_list = modal.querySelector('.features-list');
let features_shown = features_list.textContent.split('\n').map(s => s.trim()).filter(s => s);
expect(features_shown.join(' ')).toBe(
@ -1661,7 +1661,7 @@ describe("Groupchats", function () {
expect(view.model.features.get('unsecured')).toBe(false);
await u.waitUntil(() => view.querySelector('.chatbox-title__text').textContent.trim() === 'Room');
modal.el.querySelector('.close').click();
modal.querySelector('.close').click();
view.querySelector('.configure-chatroom-button').click();
const IQs = _converse.connection.IQ_stanzas;
@ -1792,10 +1792,10 @@ describe("Groupchats", function () {
await u.waitUntil(() => new Promise(success => view.model.features.on('change', success)));
info_el.click();
modal = _converse.api.modal.get('muc-details-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
modal = _converse.api.modal.get('converse-muc-details-modal');
await u.waitUntil(() => u.isVisible(modal), 1000);
features_list = modal.el.querySelector('.features-list');
features_list = modal.querySelector('.features-list');
features_shown = features_list.textContent.split('\n').map(s => s.trim()).filter(s => s);
expect(features_shown.join(' ')).toBe(
'Password protected - This groupchat requires a password before entry '+

View File

@ -19,17 +19,17 @@ describe("A MUC", function () {
const dropdown_item = view.querySelector(".open-nickname-modal");
dropdown_item.click();
const modal = _converse.api.modal.get('change-nickname-modal');
await u.waitUntil(() => u.isVisible(modal.el));
const modal = _converse.api.modal.get('converse-muc-nickname-modal');
await u.waitUntil(() => u.isVisible(modal));
const input = modal.el.querySelector('input[name="nick"]');
const input = modal.querySelector('input[name="nick"]');
expect(input.value).toBe(nick);
const newnick = 'loverboy';
input.value = newnick;
modal.el.querySelector('input[type="submit"]')?.click();
modal.querySelector('input[type="submit"]')?.click();
await u.waitUntil(() => !u.isVisible(modal.el));
await u.waitUntil(() => !u.isVisible(modal));
const { sent_stanzas } = _converse.connection;
const sent_stanza = sent_stanzas.pop()
@ -422,13 +422,13 @@ describe("A MUC", function () {
const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
roomspanel.querySelector('.show-add-muc-modal').click();
mock.closeControlBox(_converse);
const modal = _converse.api.modal.get('add-chatroom-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000)
const name_input = modal.el.querySelector('input[name="chatroom"]');
const modal = _converse.api.modal.get('converse-add-muc-modal');
await u.waitUntil(() => u.isVisible(modal), 1000)
const name_input = modal.querySelector('input[name="chatroom"]');
name_input.value = 'lounge@montague.lit';
expect(modal.el.querySelector('label[for="nickname"]')).toBe(null);
expect(modal.el.querySelector('input[name="nickname"]')).toBe(null);
modal.el.querySelector('form input[type="submit"]').click();
expect(modal.querySelector('label[for="nickname"]')).toBe(null);
expect(modal.querySelector('input[name="nickname"]')).toBe(null);
modal.querySelector('form input[type="submit"]').click();
await u.waitUntil(() => _converse.chatboxes.length > 1);
const chatroom = _converse.chatboxes.get('lounge@montague.lit');
expect(chatroom.get('nick')).toBe('romeo');
@ -442,11 +442,11 @@ describe("A MUC", function () {
const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
roomspanel.querySelector('.show-add-muc-modal').click();
mock.closeControlBox(_converse);
const modal = _converse.api.modal.get('add-chatroom-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000)
const label_nick = modal.el.querySelector('label[for="nickname"]');
const modal = _converse.api.modal.get('converse-add-muc-modal');
await u.waitUntil(() => u.isVisible(modal), 1000)
const label_nick = modal.querySelector('label[for="nickname"]');
expect(label_nick.textContent.trim()).toBe('Nickname:');
const nick_input = modal.el.querySelector('input[name="nickname"]');
const nick_input = modal.querySelector('input[name="nickname"]');
expect(nick_input.value).toBe('romeo');
}));
@ -458,11 +458,11 @@ describe("A MUC", function () {
const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
roomspanel.querySelector('.show-add-muc-modal').click();
mock.closeControlBox(_converse);
const modal = _converse.api.modal.get('add-chatroom-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000)
const label_nick = modal.el.querySelector('label[for="nickname"]');
const modal = _converse.api.modal.get('converse-add-muc-modal');
await u.waitUntil(() => u.isVisible(modal), 1000)
const label_nick = modal.querySelector('label[for="nickname"]');
expect(label_nick.textContent.trim()).toBe('Nickname:');
const nick_input = modal.el.querySelector('input[name="nickname"]');
const nick_input = modal.querySelector('input[name="nickname"]');
expect(nick_input.value).toBe('st.nick');
}));
});

View File

@ -1,5 +1,5 @@
import ModeratorToolsModal from './modals/moderator-tools.js';
import OccupantModal from './modals/occupant.js';
import './modals/occupant.js';
import './modals/moderator-tools.js';
import log from "@converse/headless/log";
import tpl_spinner from 'templates/spinner.js';
import { __ } from 'i18n';
@ -234,19 +234,19 @@ export function showModeratorToolsModal (muc, affiliation) {
if (!muc.verifyRoles(['moderator'])) {
return;
}
let modal = api.modal.get(ModeratorToolsModal.id);
let modal = api.modal.get('converse-modtools-modal');
if (modal) {
modal.affiliation = affiliation;
modal.render();
} else {
modal = api.modal.create(ModeratorToolsModal, { affiliation, 'jid': muc.get('jid') });
modal = api.modal.create('converse-modtools-modal', { affiliation, 'jid': muc.get('jid') });
}
modal.show();
}
export function showOccupantModal (ev, occupant) {
api.modal.show(OccupantModal, { 'model': occupant }, ev);
api.modal.show('converse-muc-occupant-modal', { 'model': occupant }, ev);
}

View File

@ -4,7 +4,7 @@
*/
import './fingerprints.js';
import './profile.js';
import 'modals/user-details.js';
import 'shared/modals/user-details.js';
import 'plugins/profile/index.js';
import ConverseMixins from './mixins/converse.js';
import Device from './device.js';

View File

@ -11,7 +11,7 @@ const fingerprint = (el) => html`
const device_with_fingerprint = (el) => {
const i18n_fingerprint_checkbox_label = __('Checkbox for selecting the following fingerprint');
return html`
<li class="fingerprint-removal-item list-group-item nopadding">
<li class="fingerprint-removal-item list-group-item">
<label>
<input type="checkbox" value="${el.device.get('id')}"
aria-label="${i18n_fingerprint_checkbox_label}"/>
@ -26,7 +26,7 @@ const device_without_fingerprint = (el) => {
const i18n_device_without_fingerprint = __('Device without a fingerprint');
const i18n_fingerprint_checkbox_label = __('Checkbox for selecting the following device');
return html`
<li class="fingerprint-removal-item list-group-item nopadding">
<li class="fingerprint-removal-item list-group-item">
<label>
<input type="checkbox" value="${el.device.get('id')}"
aria-label="${i18n_fingerprint_checkbox_label}"/>
@ -49,7 +49,7 @@ const device_list = (el) => {
const i18n_select_all = __('Select all');
return html`
<ul class="list-group fingerprints">
<li class="list-group-item nopadding active">
<li class="list-group-item active">
<label>
<input type="checkbox" class="select-all" @change=${el.selectAll} title="${i18n_select_all}" aria-label="${i18n_other_devices_label}"/>
${i18n_other_devices}

View File

@ -1047,8 +1047,8 @@ describe("The OMEMO module", function() {
const view = _converse.chatboxviews.get(contact_jid);
const show_modal_button = view.querySelector('.show-user-details-modal');
show_modal_button.click();
const modal = _converse.api.modal.get('user-details-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
const modal = _converse.api.modal.get('converse-user-details-modal');
await u.waitUntil(() => u.isVisible(modal), 1000);
let iq_stanza = await u.waitUntil(() => mock.deviceListFetched(_converse, contact_jid));
expect(Strophe.serialize(iq_stanza)).toBe(
@ -1068,7 +1068,7 @@ describe("The OMEMO module", function() {
.c('device', {'id': '555'})
));
await u.waitUntil(() => u.isVisible(modal.el), 1000);
await u.waitUntil(() => u.isVisible(modal), 1000);
iq_stanza = await u.waitUntil(() => mock.bundleFetched(_converse, contact_jid, '555'));
expect(Strophe.serialize(iq_stanza)).toBe(
@ -1097,21 +1097,21 @@ describe("The OMEMO module", function() {
.c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1003'))
));
await u.waitUntil(() => modal.el.querySelectorAll('.fingerprints .fingerprint').length);
expect(modal.el.querySelectorAll('.fingerprints .fingerprint').length).toBe(1);
const el = modal.el.querySelector('.fingerprints .fingerprint');
await u.waitUntil(() => modal.querySelectorAll('.fingerprints .fingerprint').length);
expect(modal.querySelectorAll('.fingerprints .fingerprint').length).toBe(1);
const el = modal.querySelector('.fingerprints .fingerprint');
expect(el.textContent.trim()).toBe(
omemo.formatFingerprint(u.arrayBufferToHex(u.base64ToArrayBuffer('BQmHEOHjsYm3w5M8VqxAtqJmLCi7CaxxsdZz6G0YpuMI')))
);
expect(modal.el.querySelectorAll('input[type="radio"]').length).toBe(2);
expect(modal.querySelectorAll('input[type="radio"]').length).toBe(2);
const devicelist = _converse.devicelists.get(contact_jid);
expect(devicelist.devices.get('555').get('trusted')).toBe(0);
let trusted_radio = modal.el.querySelector('input[type="radio"][name="555"][value="1"]');
let trusted_radio = modal.querySelector('input[type="radio"][name="555"][value="1"]');
expect(trusted_radio.checked).toBe(true);
let untrusted_radio = modal.el.querySelector('input[type="radio"][name="555"][value="-1"]');
let untrusted_radio = modal.querySelector('input[type="radio"][name="555"][value="-1"]');
expect(untrusted_radio.checked).toBe(false);
// Test that the device can be set to untrusted

View File

@ -3,11 +3,12 @@
* @license Mozilla Public License (MPLv2)
*/
import '../modal/index.js';
import './modals/chat-status.js';
import './modals/profile.js';
import './modals/user-settings.js';
import './statusview.js';
import '@converse/headless/plugins/status';
import '@converse/headless/plugins/vcard';
import './modals/chat-status.js';
import './modals/profile.js';
import { api, converse } from '@converse/headless/core';

View File

@ -1,51 +1,37 @@
import BootstrapModal from "plugins/modal/base.js";
import BaseModal from "plugins/modal/modal.js";
import tpl_chat_status_modal from "../templates/chat-status-modal.js";
import { __ } from 'i18n';
import { _converse, converse } from "@converse/headless/core";
import { _converse, api, converse } from "@converse/headless/core";
const u = converse.env.utils;
const ChatStatusModal = BootstrapModal.extend({
id: "modal-status-change",
events: {
"submit form#set-xmpp-status": "onFormSubmitted",
"click .clear-input": "clearStatusMessage"
},
export default class ChatStatusModal extends BaseModal {
toHTML () {
return tpl_chat_status_modal(
Object.assign(
this.model.toJSON(),
this.model.vcard.toJSON(), {
'label_away': __('Away'),
'label_busy': __('Busy'),
'label_cancel': __('Cancel'),
'label_close': __('Close'),
'label_custom_status': __('Custom status'),
'label_offline': __('Offline'),
'label_online': __('Online'),
'label_save': __('Save'),
'label_xa': __('Away for long'),
'modal_title': __('Change chat status'),
'placeholder_status_message': __('Personal status message')
}));
},
afterRender () {
this.el.addEventListener('shown.bs.modal', () => {
this.el.querySelector('input[name="status_message"]').focus();
initialize () {
super.initialize();
this.render();
this.addEventListener('shown.bs.modal', () => {
this.querySelector('input[name="status_message"]').focus();
}, false);
},
}
renderModal () {
return tpl_chat_status_modal(this);
}
getModalTitle () { // eslint-disable-line class-methods-use-this
return __('Change chat status');
}
clearStatusMessage (ev) {
if (ev && ev.preventDefault) {
ev.preventDefault();
u.hideElement(this.el.querySelector('.clear-input'));
u.hideElement(this.querySelector('.clear-input'));
}
const roster_filter = this.el.querySelector('input[name="status_message"]');
const roster_filter = this.querySelector('input[name="status_message"]');
roster_filter.value = '';
},
}
onFormSubmitted (ev) {
ev.preventDefault();
@ -56,9 +42,8 @@ const ChatStatusModal = BootstrapModal.extend({
});
this.modal.hide();
}
});
}
_converse.ChatStatusModal = ChatStatusModal;
export default ChatStatusModal;
api.elements.define('converse-chat-status-modal', ChatStatusModal);

View File

@ -1,52 +1,43 @@
import BootstrapModal from "plugins/modal/base.js";
import bootstrap from "bootstrap.native";
import BaseModal from "plugins/modal/modal.js";
import log from "@converse/headless/log";
import tpl_profile_modal from "../templates/profile_modal.js";
import Compress from 'client-compress';
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
import { _converse, api } from "@converse/headless/core";
const { sizzle } = converse.env;
const compress = new Compress({
targetSize: 0.1,
quality: 0.75,
maxWidth: 256,
maxHeight: 256
});
const options = {
targetSize: 0.1,
quality: 0.75,
maxWidth: 256,
maxHeight: 256
}
export default class ProfileModal extends BaseModal {
const compress = new Compress(options)
const ProfileModal = BootstrapModal.extend({
id: "user-profile-modal",
events: {
'submit .profile-form': 'onFormSubmitted'
},
constructor (options) {
super(options);
this.tab = 'profile';
}
initialize () {
super.initialize();
this.listenTo(this.model, 'change', this.render);
BootstrapModal.prototype.initialize.apply(this, arguments);
/**
* Triggered when the _converse.ProfileModal has been created and initialized.
* @event _converse#profileModalInitialized
* @type { _converse.XMPPStatus }
* @example _converse.api.listen.on('profileModalInitialized', status => { ... });
*/
* Triggered when the _converse.ProfileModal has been created and initialized.
* @event _converse#profileModalInitialized
* @type { _converse.XMPPStatus }
* @example _converse.api.listen.on('profileModalInitialized', status => { ... });
*/
api.trigger('profileModalInitialized', this.model);
},
}
toHTML () {
return tpl_profile_modal(Object.assign(
this.model.toJSON(),
this.model.vcard.toJSON(),
{ 'view': this }
));
},
renderModal () {
return tpl_profile_modal(this);
}
afterRender () {
this.tabs = sizzle('.nav-item .nav-link', this.el).map(e => new bootstrap.Tab(e));
},
getModalTitle () { // eslint-disable-line class-methods-use-this
return __('Your Profile');
}
async setVCard (data) {
try {
@ -60,7 +51,7 @@ const ProfileModal = BootstrapModal.extend({
return;
}
this.modal.hide();
},
}
onFormSubmitted (ev) {
ev.preventDefault();
@ -95,8 +86,6 @@ const ProfileModal = BootstrapModal.extend({
});
}
}
});
}
_converse.ProfileModal = ProfileModal;
export default ProfileModal;
api.elements.define('converse-profile-modal', ProfileModal);

View File

@ -0,0 +1,38 @@
converse-profile-modal {
.profile-form {
label {
font-weight: bold;
}
}
.fingerprint-removal {
label {
display: flex;
padding: 0.75rem 1.25rem;
}
}
.list-group-item {
display: flex;
justify-content: left;
font-size: 95%;
input[type="checkbox"] {
margin-right: 1em;
}
}
.fingerprints {
width: 100%;
margin-bottom: 1em;
}
.fingerprint-trust {
display: flex;
justify-content: space-between;
font-size: 95%;
.fingerprint {
margin-left: 1em;
}
}
}

View File

@ -1,29 +1,41 @@
import DOMPurify from 'dompurify';
import { __ } from 'i18n';
import { api } from "@converse/headless/core";
import { _converse, api } from "@converse/headless/core.js";
import { html } from "lit";
import { modal_header_close_button } from "plugins/modal/templates/buttons.js"
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
const tpl_navigation = (o) => {
const tpl_navigation = (el) => {
const i18n_about = __('About');
const i18n_commands = __('Commands');
return html`
<ul class="nav nav-pills justify-content-center">
<li role="presentation" class="nav-item">
<a class="nav-link active" id="about-tab" href="#about-tabpanel" aria-controls="about-tabpanel" role="tab" data-toggle="tab" @click=${o.switchTab}>${i18n_about}</a>
<a class="nav-link ${el.tab === "about" ? "active" : ""}"
id="about-tab"
href="#about-tabpanel"
aria-controls="about-tabpanel"
role="tab"
data-toggle="tab"
data-name="about"
@click=${ev => el.switchTab(ev)}>${i18n_about}</a>
</li>
<li role="presentation" class="nav-item">
<a class="nav-link" id="commands-tab" href="#commands-tabpanel" aria-controls="commands-tabpanel" role="tab" data-toggle="tab" @click=${o.switchTab}>${i18n_commands}</a>
<a class="nav-link ${el.tab === "commands" ? "active" : ""}"
id="commands-tab"
href="#commands-tabpanel"
aria-controls="commands-tabpanel"
role="tab"
data-toggle="tab"
data-name="commands"
@click=${ev => el.switchTab(ev)}>${i18n_commands}</a>
</li>
</ul>
`;
}
export default (o) => {
const i18n_modal_title = __('Settings');
export default (el) => {
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">',
@ -39,38 +51,31 @@ export default (o) => {
const show_client_info = api.settings.get('show_client_info');
const allow_adhoc_commands = api.settings.get('allow_adhoc_commands');
const show_both_tabs = show_client_info && allow_adhoc_commands;
return 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_modal_title}</h5>
${modal_header_close_button}
</div>
<div class="modal-body">
${ show_both_tabs ? tpl_navigation(o) : '' }
${ show_both_tabs ? tpl_navigation(el) : '' }
<div class="tab-content">
<div class="tab-pane tab-pane--columns ${show_client_info ? 'active' : ''}"
id="about-tabpanel" role="tabpanel" aria-labelledby="about-tab">
<div class="tab-content">
${ show_client_info ? html`
<div class="tab-pane tab-pane--columns ${ el.tab === 'about' ? 'active' : ''}"
id="about-tabpanel" role="tabpanel" aria-labelledby="about-tab">
<span class="modal-alert"></span>
<br/>
<div class="container">
<h6 class="brand-heading">Converse</h6>
<p class="brand-subtitle">${o.version_name}</p>
<p class="brand-subtitle">${unsafeHTML(DOMPurify.sanitize(first_subtitle))}</p>
<p class="brand-subtitle">${unsafeHTML(DOMPurify.sanitize(second_subtitle))}</p>
</div>
<span class="modal-alert"></span>
<br/>
<div class="container">
<h6 class="brand-heading">Converse</h6>
<p class="brand-subtitle">${_converse.VERSION_NAME}</p>
<p class="brand-subtitle">${unsafeHTML(DOMPurify.sanitize(first_subtitle))}</p>
<p class="brand-subtitle">${unsafeHTML(DOMPurify.sanitize(second_subtitle))}</p>
</div>
</div>` : '' }
<div class="tab-pane tab-pane--columns ${!show_client_info && allow_adhoc_commands ? 'active' : ''}"
id="commands-tabpanel"
role="tabpanel"
aria-labelledby="commands-tab">
<converse-adhoc-commands/>
</div>
</div>
</div>
${ allow_adhoc_commands ? html`
<div class="tab-pane tab-pane--columns ${ el.tab === 'commands' ? 'active' : ''}"
id="commands-tabpanel"
role="tabpanel"
aria-labelledby="commands-tab">
<converse-adhoc-commands/>
</div> ` : '' }
</div>
</div>
`};

View File

@ -1,23 +1,31 @@
import BootstrapModal from "plugins/modal/base.js";
import BaseModal from "plugins/modal/modal.js";
import tpl_user_settings_modal from "./templates/user-settings.js";
import { __ } from 'i18n';
import { api } from "@converse/headless/core";
let _converse;
export default class UserSettingsModal extends BaseModal {
export default BootstrapModal.extend({
id: "converse-client-info-modal",
constructor (options) {
super(options);
initialize (settings) {
_converse = settings._converse;
BootstrapModal.prototype.initialize.apply(this, arguments);
},
const show_client_info = api.settings.get('show_client_info');
const allow_adhoc_commands = api.settings.get('allow_adhoc_commands');
const show_both_tabs = show_client_info && allow_adhoc_commands;
toHTML () {
return tpl_user_settings_modal(
Object.assign(
this.model.toJSON(),
this.model.vcard.toJSON(),
{ 'version_name': _converse.VERSION_NAME }
)
);
if (show_both_tabs || show_client_info) {
this.tab = 'about';
} else if (allow_adhoc_commands) {
this.tab = 'commands';
}
}
});
renderModal () {
return tpl_user_settings_modal(this);
}
getModalTitle () { // eslint-disable-line class-methods-use-this
return __('Settings');
}
}
api.elements.define('converse-user-settings-modal', UserSettingsModal);

View File

@ -1,10 +1,8 @@
import UserSettingsModal from './modals/user-settings';
import tpl_profile from './templates/profile.js';
import { CustomElement } from 'shared/components/element.js';
import { _converse, api } from '@converse/headless/core';
class Profile extends CustomElement {
initialize () {
this.model = _converse.xmppstatus;
this.listenTo(this.model, "change", () => this.requestUpdate());
@ -18,17 +16,17 @@ class Profile extends CustomElement {
showProfileModal (ev) {
ev?.preventDefault();
api.modal.show(_converse.ProfileModal, {model: this.model}, ev);
api.modal.show('converse-profile-modal', { model: this.model }, ev);
}
showStatusChangeModal (ev) {
ev?.preventDefault();
api.modal.show(_converse.ChatStatusModal, {model: this.model}, ev);
api.modal.show('converse-chat-status-modal', { model: this.model }, ev);
}
showUserSettingsModal(ev) {
showUserSettingsModal (ev) {
ev?.preventDefault();
api.modal.show(UserSettingsModal, {model: this.model, _converse}, ev);
api.modal.show('converse-user-settings-modal', { model: this.model, _converse }, ev);
}
}

View File

@ -1,53 +1,52 @@
import { html } from "lit";
import { modal_header_close_button } from "plugins/modal/templates/buttons.js";
import { __ } from 'i18n';
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>
${modal_header_close_button}
export default (el) => {
const label_away = __('Away');
const label_busy = __('Busy');
const label_online = __('Online');
const label_save = __('Save');
const label_xa = __('Away for long');
const placeholder_status_message = __('Personal status message');
const status = el.model.get('status');
const status_message = el.model.get('status_message');
return html`
<form class="converse-form set-xmpp-status" id="set-xmpp-status" @submit=${ev => el.onFormSubmitted(ev)}>
<div class="form-group">
<div class="custom-control custom-radio">
<input ?checked=${status === 'online'}
type="radio" id="radio-online" value="online" name="chat_status" class="custom-control-input"/>
<label class="custom-control-label" for="radio-online">
<converse-icon size="1em" class="fa fa-circle chat-status chat-status--online"></converse-icon>${label_online}</label>
</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 ?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">
<converse-icon size="1em" class="fa fa-circle chat-status chat-status--online"></converse-icon>${o.label_online}</label>
</div>
<div class="custom-control custom-radio">
<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">
<converse-icon size="1em" class="fa fa-minus-circle chat-status chat-status--busy"></converse-icon>${o.label_busy}</label>
</div>
<div class="custom-control custom-radio">
<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">
<converse-icon size="1em" class="fa fa-circle chat-status chat-status--away"></converse-icon>${o.label_away}</label>
</div>
<div class="custom-control custom-radio">
<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">
<converse-icon size="1em" class="far fa-circle chat-status chat-status--xa"></converse-icon>${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}"/>
<converse-icon size="1em" class="fa fa-times clear-input ${o.status_message ? '' : 'hidden'}"></converse-icon>
</div>
</div>
<button type="submit" class="btn btn-primary">${o.label_save}</button>
</form>
<div class="custom-control custom-radio">
<input ?checked=${status === 'busy'}
type="radio" id="radio-busy" value="dnd" name="chat_status" class="custom-control-input"/>
<label class="custom-control-label" for="radio-busy">
<converse-icon size="1em" class="fa fa-minus-circle chat-status chat-status--busy"></converse-icon>${label_busy}</label>
</div>
<div class="custom-control custom-radio">
<input ?checked=${status === 'away'}
type="radio" id="radio-away" value="away" name="chat_status" class="custom-control-input"/>
<label class="custom-control-label" for="radio-away">
<converse-icon size="1em" class="fa fa-circle chat-status chat-status--away"></converse-icon>${label_away}</label>
</div>
<div class="custom-control custom-radio">
<input ?checked=${status === 'xa'}
type="radio" id="radio-xa" value="xa" name="chat_status" class="custom-control-input"/>
<label class="custom-control-label" for="radio-xa">
<converse-icon size="1em" class="far fa-circle chat-status chat-status--xa"></converse-icon>${label_xa}</label>
</div>
</div>
</div>
`;
<div class="form-group">
<div class="btn-group w-100">
<input name="status_message" type="text" class="form-control" autofocus
value="${status_message || ''}" placeholder="${placeholder_status_message}"/>
<converse-icon size="1em" class="fa fa-times clear-input ${status_message ? '' : 'hidden'}" @click=${ev => el.clearStatusMessage(ev)}></converse-icon>
</div>
</div>
<button type="submit" class="btn btn-primary">${label_save}</button>
</form>`;
}

View File

@ -2,16 +2,40 @@ import "shared/components/image-picker.js";
import { __ } from 'i18n';
import { _converse } from "@converse/headless/core";
import { html } from "lit";
import { modal_header_close_button } from "plugins/modal/templates/buttons.js";
const omemo_page = () => html`
<div class="tab-pane" id="omemo-tabpanel" role="tabpanel" aria-labelledby="omemo-tab">
const omemo_page = (el) => html`
<div class="tab-pane ${ el.tab === 'omemo' ? 'active' : ''}" id="omemo-tabpanel" role="tabpanel" aria-labelledby="omemo-tab">
<converse-omemo-profile></converse-omemo-profile>
</div>`;
const navigation = (el) => {
const i18n_omemo = __('OMEMO');
const i18n_profile = __('Profile');
export default (o) => {
const heading_profile = __('Your Profile');
return html`<ul class="nav nav-pills justify-content-center">
<li role="presentation" class="nav-item">
<a class="nav-link ${el.tab === "profile" ? "active" : ""}"
id="profile-tab"
href="#profile-tabpanel"
aria-controls="profile-tabpanel" role="tab"
@click=${ev => el.switchTab(ev)}
data-name="profile"
data-toggle="tab">${i18n_profile}</a>
</li>
<li role="presentation" class="nav-item">
<a class="nav-link ${el.tab === "omemo" ? "active" : ""}"
id="omemo-tab"
href="#omemo-tabpanel"
aria-controls="omemo-tabpanel" role="tab"
@click=${ev => el.switchTab(ev)}
data-name="omemo"
data-toggle="tab">${i18n_omemo}</a>
</li>
</ul>`;
}
export default (el) => {
const o = { ...el.model.toJSON(), ...el.model.vcard.toJSON() };
const i18n_email = __('Email');
const i18n_fullname = __('Full Name');
const i18n_jid = __('XMPP Address');
@ -20,74 +44,51 @@ export default (o) => {
const i18n_save = __('Save and close');
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>`;
return 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>
${_converse.pluggable.plugins['converse-omemo']?.enabled(_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">
<converse-image-picker .data="${{image: o.image, image_type: o.image_type}}" width="128" height="128"></converse-image-picker>
</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>
${_converse.pluggable.plugins['converse-omemo']?.enabled(_converse) ? navigation(el) : ''}
<div class="tab-content">
<div class="tab-pane ${ el.tab === 'profile' ? 'active' : ''}" id="profile-tabpanel" role="tabpanel" aria-labelledby="profile-tab">
<form class="converse-form converse-form--modal profile-form" action="#" @submit=${ev => el.onFormSubmitted(ev)}>
<div class="row">
<div class="col-auto">
<converse-image-picker .data="${{image: o.image, image_type: o.image_type}}" width="128" height="128"></converse-image-picker>
</div>
<div class="col">
<div class="form-group">
<label class="col-form-label">${i18n_jid}:</label>
<div>${o.jid}</div>
</div>
</div>
${ _converse.pluggable.plugins['converse-omemo']?.enabled(_converse) ? omemo_page() : '' }
</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(el) : '' }
</div>
`;
}

View File

@ -1,5 +1,5 @@
import AddMUCModal from 'plugins/muc-views/modals/add-muc.js';
import MUCListModal from 'plugins/muc-views/modals/muc-list.js';
import 'plugins/muc-views/modals/add-muc.js';
import 'plugins/muc-views/modals/muc-list.js';
import { __ } from 'i18n';
import { _converse, api } from "@converse/headless/core";
import { html } from "lit";
@ -80,12 +80,12 @@ export default (o) => {
<div class="d-flex controlbox-padded">
<span class="w-100 controlbox-heading controlbox-heading--groupchats">${i18n_heading_chatrooms}</span>
<a class="controlbox-heading__btn show-list-muc-modal"
@click=${(ev) => api.modal.show(MUCListModal, { 'model': o.model }, ev)}
@click=${(ev) => api.modal.show('converse-muc-list-modal', { 'model': o.model }, ev)}
title="${i18n_title_list_rooms}" data-toggle="modal" data-target="#muc-list-modal">
<converse-icon class="fa fa-list-ul right" size="1em"></converse-icon>
</a>
<a class="controlbox-heading__btn show-add-muc-modal"
@click=${(ev) => api.modal.show(AddMUCModal, { 'model': o.model }, ev)}
@click=${(ev) => api.modal.show('converse-add-muc-modal', { 'model': o.model }, ev)}
title="${i18n_title_new_room}" data-toggle="modal" data-target="#add-chatrooms-modal">
<converse-icon class="fa fa-plus right" size="1em"></converse-icon>
</a>

View File

@ -255,9 +255,9 @@ describe("A groupchat shown in the groupchats list", function () {
const info_el = rooms_list.querySelector(".room-info");
info_el.click();
const modal = _converse.api.modal.get('muc-details-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
let els = modal.el.querySelectorAll('p.room-info');
const modal = _converse.api.modal.get('converse-muc-details-modal');
await u.waitUntil(() => u.isVisible(modal), 1000);
let els = modal.querySelectorAll('p.room-info');
expect(els[0].textContent).toBe("Name: A Dark Cave")
expect(els[1].querySelector('strong').textContent).toBe("XMPP address");
@ -266,7 +266,7 @@ describe("A groupchat shown in the groupchats list", function () {
expect(els[2].querySelector('converse-rich-text').textContent).toBe("This is the description");
expect(els[3].textContent).toBe("Online users: 1")
const features_list = modal.el.querySelector('.features-list');
const features_list = modal.querySelector('.features-list');
expect(features_list.textContent.replace(/(\n|\s{2,})/g, '')).toBe(
'Password protected - This groupchat requires a password before entry'+
'Hidden - This groupchat is not publicly searchable'+
@ -287,11 +287,11 @@ describe("A groupchat shown in the groupchats list", function () {
});
_converse.connection._dataRecv(mock.createRequest(presence));
els = modal.el.querySelectorAll('p.room-info');
els = modal.querySelectorAll('p.room-info');
expect(els[3].textContent).toBe("Online users: 2")
view.model.set({'subject': {'author': 'someone', 'text': 'Hatching dark plots'}});
els = modal.el.querySelectorAll('p.room-info');
els = modal.querySelectorAll('p.room-info');
expect(els[0].textContent).toBe("Name: A Dark Cave")
expect(els[1].querySelector('strong').textContent).toBe("XMPP address");

View File

@ -1,4 +1,4 @@
import RoomDetailsModal from 'plugins/muc-views/modals/muc-details.js';
import 'plugins/muc-views/modals/muc-details.js';
import RoomsListModel from './model.js';
import tpl_roomslist from "./templates/roomslist.js";
import { CustomElement } from 'shared/components/element.js';
@ -58,7 +58,7 @@ export class RoomsList extends CustomElement {
const jid = ev.currentTarget.getAttribute('data-room-jid');
const room = _converse.chatboxes.get(jid);
ev.preventDefault();
api.modal.show(RoomDetailsModal, {'model': room}, ev);
api.modal.show('converse-muc-details-modal', {'model': room}, ev);
}
async openRoom (ev) { // eslint-disable-line class-methods-use-this

View File

@ -1,5 +1,5 @@
import 'shared/autocomplete/index.js';
import BootstrapModal from "plugins/modal/base.js";
import BaseModal from "plugins/modal/modal.js";
import compact from 'lodash-es/compact';
import debounce from 'lodash-es/debounce';
import tpl_add_contact_modal from "./templates/add-contact.js";
@ -9,18 +9,22 @@ import { _converse, api, converse } from "@converse/headless/core";
const { Strophe } = converse.env;
const u = converse.env.utils;
const AddContactModal = BootstrapModal.extend({
id: "add-contact-modal",
export default class AddContactModal extends BaseModal {
initialize () {
BootstrapModal.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change', this.render);
},
super.initialize();
this.listenTo(this.model, 'change', () => this.render());
this.render();
this.addEventListener('shown.bs.modal', () => this.querySelector('input[name="jid"]')?.focus(), false);
}
toHTML () {
renderModal () {
return tpl_add_contact_modal(this);
},
}
getModalTitle () { // eslint-disable-line class-methods-use-this
return __('Add a Contact');
}
afterRender () {
if (typeof api.settings.get('xhr_user_search_url') === 'string') {
@ -28,39 +32,37 @@ const AddContactModal = BootstrapModal.extend({
} else {
this.initJIDAutoComplete();
}
const jid_input = this.el.querySelector('input[name="jid"]');
this.el.addEventListener('shown.bs.modal', () => jid_input.focus(), false);
},
}
initJIDAutoComplete () {
if (!api.settings.get('autocomplete_add_contact')) {
return;
}
const el = this.el.querySelector('.suggestion-box__jid').parentElement;
const el = this.querySelector('.suggestion-box__jid').parentElement;
this.jid_auto_complete = new _converse.AutoComplete(el, {
'data': (text, input) => `${input.slice(0, input.indexOf("@"))}@${text}`,
'filter': _converse.FILTER_STARTSWITH,
'list': [...new Set(_converse.roster.map(item => Strophe.getDomainFromJid(item.get('jid'))))]
});
},
}
initGroupAutoComplete () {
if (!api.settings.get('autocomplete_add_contact')) {
return;
}
const el = this.el.querySelector('.suggestion-box__jid').parentElement;
const el = this.querySelector('.suggestion-box__jid').parentElement;
this.jid_auto_complete = new _converse.AutoComplete(el, {
'data': (text, input) => `${input.slice(0, input.indexOf("@"))}@${text}`,
'filter': _converse.FILTER_STARTSWITH,
'list': [...new Set(_converse.roster.map(item => Strophe.getDomainFromJid(item.get('jid'))))]
});
},
}
initXHRAutoComplete () {
if (!api.settings.get('autocomplete_add_contact')) {
return this.initXHRFetch();
}
const el = this.el.querySelector('.suggestion-box__name').parentElement;
const el = this.querySelector('.suggestion-box__name').parentElement;
this.name_auto_complete = new _converse.AutoComplete(el, {
'auto_evaluate': false,
'filter': _converse.FILTER_STARTSWITH,
@ -76,16 +78,16 @@ const AddContactModal = BootstrapModal.extend({
this.name_auto_complete.evaluate();
}
};
const input_el = this.el.querySelector('input[name="name"]');
const input_el = this.querySelector('input[name="name"]');
input_el.addEventListener('input', debounce(() => {
xhr.open("GET", `${api.settings.get('xhr_user_search_url')}q=${encodeURIComponent(input_el.value)}`, true);
xhr.send()
} , 300));
this.name_auto_complete.on('suggestion-box-selectcomplete', ev => {
this.el.querySelector('input[name="name"]').value = ev.text.label;
this.el.querySelector('input[name="jid"]').value = ev.text.value;
this.querySelector('input[name="name"]').value = ev.text.label;
this.querySelector('input[name="jid"]').value = ev.text.value;
});
},
}
initXHRFetch () {
this.xhr = new window.XMLHttpRequest();
@ -94,25 +96,25 @@ const AddContactModal = BootstrapModal.extend({
const r = this.xhr.responseText;
const list = JSON.parse(r).map(i => ({'label': i.fullname || i.jid, 'value': i.jid}));
if (list.length !== 1) {
const el = this.el.querySelector('.invalid-feedback');
const el = this.querySelector('.invalid-feedback');
el.textContent = __('Sorry, could not find a contact with that name')
u.addClass('d-block', el);
return;
}
const jid = list[0].value;
if (this.validateSubmission(jid)) {
const form = this.el.querySelector('form');
const form = this.querySelector('form');
const name = list[0].label;
this.afterSubmission(form, jid, name);
}
}
};
},
}
validateSubmission (jid) {
const el = this.el.querySelector('.invalid-feedback');
const el = this.querySelector('.invalid-feedback');
if (!jid || compact(jid.split('@')).length < 2) {
u.addClass('is-invalid', this.el.querySelector('input[name="jid"]'));
u.addClass('is-invalid', this.querySelector('input[name="jid"]'));
u.addClass('d-block', el);
return false;
} else if (_converse.roster.get(Strophe.getBareJidFromJid(jid))) {
@ -122,16 +124,16 @@ const AddContactModal = BootstrapModal.extend({
}
u.removeClass('d-block', el);
return true;
},
}
afterSubmission (form, jid, name, group) {
afterSubmission (_form, jid, name, group) {
if (group && !Array.isArray(group)) {
group = [group];
}
_converse.roster.addAndSubscribe(jid, name, group);
this.model.clear();
this.modal.hide();
},
}
addContactFromForm (ev) {
ev.preventDefault();
@ -139,7 +141,7 @@ const AddContactModal = BootstrapModal.extend({
const jid = (data.get('jid') || '').trim();
if (!jid && typeof api.settings.get('xhr_user_search_url') === 'string') {
const input_el = this.el.querySelector('input[name="name"]');
const input_el = this.querySelector('input[name="name"]');
this.xhr.open("GET", `${api.settings.get('xhr_user_search_url')}q=${encodeURIComponent(input_el.value)}`, true);
this.xhr.send()
return;
@ -148,8 +150,6 @@ const AddContactModal = BootstrapModal.extend({
this.afterSubmission(ev.target, jid, data.get('name'), data.get('group'));
}
}
});
}
_converse.AddContactModal = AddContactModal;
export default AddContactModal;
api.elements.define('converse-add-contact-modal', AddContactModal);

View File

@ -2,7 +2,6 @@ import { __ } from 'i18n';
import { api } from '@converse/headless/core.js';
import { getGroupsAutoCompleteList } from '@converse/headless/plugins/roster/utils.js';
import { html } from "lit";
import { modal_header_close_button } from "plugins/modal/templates/buttons.js"
export default (el) => {
@ -10,51 +9,40 @@ export default (el) => {
const i18n_contact_placeholder = __('name@example.org');
const i18n_error_message = __('Please enter a valid XMPP address');
const i18n_group = __('Group');
const i18n_new_contact = __('Add a Contact');
const i18n_nickname = __('Name');
const i18n_xmpp_address = __('XMPP Address');
return html`
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addContactModalLabel">${i18n_new_contact}</h5>
${modal_header_close_button}
</div>
<form class="converse-form add-xmpp-contact" @submit=${ev => el.addContactFromForm(ev)}>
<div class="modal-body">
<span class="modal-alert"></span>
<div class="form-group add-xmpp-contact__jid">
<label class="clearfix" for="jid">${i18n_xmpp_address}:</label>
<div class="suggestion-box suggestion-box__jid">
<ul class="suggestion-box__results suggestion-box__results--below" hidden=""></ul>
<input type="text" name="jid" ?required=${(!api.settings.get('xhr_user_search_url'))}
value="${el.model.get('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">${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="${el.model.get('nickname') || ''}"
class="form-control suggestion-box__input"/>
<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__group">
<label class="clearfix" for="name">${i18n_group}:</label>
<converse-autocomplete .list=${getGroupsAutoCompleteList()} name="group"></converse-autocomplete>
</div>
<div class="form-group"><div class="invalid-feedback">${i18n_error_message}</div></div>
<button type="submit" class="btn btn-primary">${i18n_add}</button>
<form class="converse-form add-xmpp-contact" @submit=${ev => el.addContactFromForm(ev)}>
<div class="modal-body">
<span class="modal-alert"></span>
<div class="form-group add-xmpp-contact__jid">
<label class="clearfix" for="jid">${i18n_xmpp_address}:</label>
<div class="suggestion-box suggestion-box__jid">
<ul class="suggestion-box__results suggestion-box__results--below" hidden=""></ul>
<input type="text" name="jid" ?required=${(!api.settings.get('xhr_user_search_url'))}
value="${el.model.get('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>
</form>
</div>
<div class="form-group add-xmpp-contact__name">
<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="${el.model.get('nickname') || ''}"
class="form-control suggestion-box__input"/>
<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__group">
<label class="clearfix" for="name">${i18n_group}:</label>
<converse-autocomplete .list=${getGroupsAutoCompleteList()} name="group"></converse-autocomplete>
</div>
<div class="form-group"><div class="invalid-feedback">${i18n_error_message}</div></div>
<button type="submit" class="btn btn-primary">${i18n_add}</button>
</div>
</div>
`;
</form>`;
}

View File

@ -13,13 +13,14 @@ export default class RosterView extends CustomElement {
async initialize () {
await api.waitUntil('rosterInitialized')
const { presences, roster } = _converse;
this.listenTo(_converse, 'rosterContactsFetched', () => this.requestUpdate());
this.listenTo(_converse.presences, 'change:show', () => this.requestUpdate());
this.listenTo(_converse.roster, 'add', () => this.requestUpdate());
this.listenTo(_converse.roster, 'destroy', () => this.requestUpdate());
this.listenTo(_converse.roster, 'remove', () => this.requestUpdate());
this.listenTo(_converse.roster, 'change', () => this.requestUpdate());
this.listenTo(_converse.roster.state, 'change', () => this.requestUpdate());
this.listenTo(presences, 'change:show', () => this.requestUpdate());
this.listenTo(roster, 'add', () => this.requestUpdate());
this.listenTo(roster, 'destroy', () => this.requestUpdate());
this.listenTo(roster, 'remove', () => this.requestUpdate());
this.listenTo(roster, 'change', () => this.requestUpdate());
this.listenTo(roster.state, 'change', () => this.requestUpdate());
/**
* Triggered once the _converse.RosterView instance has been created and initialized.
* @event _converse#rosterViewInitialized
@ -42,7 +43,7 @@ export default class RosterView extends CustomElement {
}
showAddContactModal (ev) { // eslint-disable-line class-methods-use-this
api.modal.show(_converse.AddContactModal, {'model': new Model()}, ev);
api.modal.show('converse-add-contact-modal', {'model': new Model()}, ev);
}
async syncContacts (ev) { // eslint-disable-line class-methods-use-this

View File

@ -0,0 +1,195 @@
/*global mock, converse */
const u = converse.env.utils;
const Strophe = converse.env.Strophe;
const sizzle = converse.env.sizzle;
describe("The 'Add Contact' widget", function () {
it("opens up an add modal when you click on it",
mock.initConverse([], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'all');
await mock.openControlBox(_converse);
const cbview = _converse.chatboxviews.get('controlbox');
cbview.querySelector('.add-contact').click()
const modal = _converse.api.modal.get('converse-add-contact-modal');
await u.waitUntil(() => u.isVisible(modal), 1000);
expect(modal.querySelector('form.add-xmpp-contact')).not.toBe(null);
const input_jid = modal.querySelector('input[name="jid"]');
const input_name = modal.querySelector('input[name="name"]');
input_jid.value = 'someone@';
const evt = new Event('input');
input_jid.dispatchEvent(evt);
expect(modal.querySelector('.suggestion-box li').textContent).toBe('someone@montague.lit');
input_jid.value = 'someone@montague.lit';
input_name.value = 'Someone';
modal.querySelector('button[type="submit"]').click();
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>`);
}));
it("can be configured to not provide search suggestions",
mock.initConverse([], {'autocomplete_add_contact': false}, async function (_converse) {
await mock.waitForRoster(_converse, 'all', 0);
await mock.openControlBox(_converse);
const cbview = _converse.chatboxviews.get('controlbox');
cbview.querySelector('.add-contact').click()
const modal = _converse.api.modal.get('converse-add-contact-modal');
expect(modal.jid_auto_complete).toBe(undefined);
expect(modal.name_auto_complete).toBe(undefined);
await u.waitUntil(() => u.isVisible(modal), 1000);
expect(modal.querySelector('form.add-xmpp-contact')).not.toBe(null);
const input_jid = modal.querySelector('input[name="jid"]');
input_jid.value = 'someone@montague.lit';
modal.querySelector('button[type="submit"]').click();
const IQ_stanzas = _converse.connection.IQ_stanzas;
const sent_stanza = await u.waitUntil(
() => 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">`+
`<query xmlns="jabber:iq:roster"><item jid="someone@montague.lit"/></query>`+
`</iq>`
);
}));
it("integrates with xhr_user_search_url to search for contacts",
mock.initConverse([], { 'xhr_user_search_url': 'http://example.org/?' },
async function (_converse) {
await mock.waitForRoster(_converse, 'all', 0);
class MockXHR extends XMLHttpRequest {
open () {} // eslint-disable-line
responseText = ''
send () {
this.responseText = JSON.stringify([
{"jid": "marty@mcfly.net", "fullname": "Marty McFly"},
{"jid": "doc@brown.com", "fullname": "Doc Brown"}
]);
this.onload();
}
}
const XMLHttpRequestBackup = window.XMLHttpRequest;
window.XMLHttpRequest = MockXHR;
await mock.openControlBox(_converse);
const cbview = _converse.chatboxviews.get('controlbox');
cbview.querySelector('.add-contact').click()
const modal = _converse.api.modal.get('converse-add-contact-modal');
await u.waitUntil(() => u.isVisible(modal), 1000);
// We only have autocomplete for the name input
expect(modal.jid_auto_complete).toBe(undefined);
expect(modal.name_auto_complete instanceof _converse.AutoComplete).toBe(true);
const input_el = modal.querySelector('input[name="name"]');
input_el.value = 'marty';
input_el.dispatchEvent(new Event('input'));
await u.waitUntil(() => modal.querySelector('.suggestion-box li'), 1000);
expect(modal.querySelectorAll('.suggestion-box li').length).toBe(1);
const suggestion = modal.querySelector('.suggestion-box li');
expect(suggestion.textContent).toBe('Marty McFly');
// Mock selection
modal.name_auto_complete.select(suggestion);
expect(input_el.value).toBe('Marty McFly');
expect(modal.querySelector('input[name="jid"]').value).toBe('marty@mcfly.net');
modal.querySelector('button[type="submit"]').click();
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;
}));
it("can be configured to not provide search suggestions for XHR search results",
mock.initConverse([],
{ 'autocomplete_add_contact': false,
'xhr_user_search_url': 'http://example.org/?' },
async function (_converse) {
await mock.waitForRoster(_converse, 'all');
await mock.openControlBox(_converse);
class MockXHR extends XMLHttpRequest {
open () {} // eslint-disable-line
responseText = ''
send () {
const value = modal.querySelector('input[name="name"]').value;
if (value === 'existing') {
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
this.responseText = JSON.stringify([{"jid": contact_jid, "fullname": mock.cur_names[0]}]);
} else if (value === 'romeo') {
this.responseText = JSON.stringify([{"jid": "romeo@montague.lit", "fullname": "Romeo Montague"}]);
} else if (value === 'ambiguous') {
this.responseText = JSON.stringify([
{"jid": "marty@mcfly.net", "fullname": "Marty McFly"},
{"jid": "doc@brown.com", "fullname": "Doc Brown"}
]);
} else if (value === 'insufficient') {
this.responseText = JSON.stringify([]);
} else {
this.responseText = JSON.stringify([{"jid": "marty@mcfly.net", "fullname": "Marty McFly"}]);
}
this.onload();
}
}
const XMLHttpRequestBackup = window.XMLHttpRequest;
window.XMLHttpRequest = MockXHR;
const cbview = _converse.chatboxviews.get('controlbox');
cbview.querySelector('.add-contact').click()
const modal = _converse.api.modal.get('converse-add-contact-modal');
await u.waitUntil(() => u.isVisible(modal), 1000);
expect(modal.jid_auto_complete).toBe(undefined);
expect(modal.name_auto_complete).toBe(undefined);
const input_el = modal.querySelector('input[name="name"]');
input_el.value = 'ambiguous';
modal.querySelector('button[type="submit"]').click();
let feedback_el = modal.querySelector('.invalid-feedback');
expect(feedback_el.textContent).toBe('Sorry, could not find a contact with that name');
feedback_el.textContent = '';
input_el.value = 'insufficient';
modal.querySelector('button[type="submit"]').click();
feedback_el = modal.querySelector('.invalid-feedback');
expect(feedback_el.textContent).toBe('Sorry, could not find a contact with that name');
feedback_el.textContent = '';
input_el.value = 'existing';
modal.querySelector('button[type="submit"]').click();
feedback_el = modal.querySelector('.invalid-feedback');
expect(feedback_el.textContent).toBe('This contact has already been added');
input_el.value = 'Marty McFly';
modal.querySelector('button[type="submit"]').click();
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

@ -17,11 +17,11 @@ describe("A sent presence stanza", function () {
const cbview = _converse.chatboxviews.get('controlbox');
const change_status_el = await u.waitUntil(() => cbview.querySelector('.change-status'));
change_status_el.click()
let modal = _converse.api.modal.get('modal-status-change');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
let modal = _converse.api.modal.get('converse-chat-status-modal');
await u.waitUntil(() => u.isVisible(modal), 1000);
const msg = 'My custom status';
modal.el.querySelector('input[name="status_message"]').value = msg;
modal.el.querySelector('[type="submit"]').click();
modal.querySelector('input[name="status_message"]').value = msg;
modal.querySelector('[type="submit"]').click();
const sent_stanzas = _converse.connection.sent_stanzas;
let sent_presence = await u.waitUntil(() => sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).pop());
@ -31,14 +31,15 @@ describe("A sent presence stanza", function () {
`<priority>0</priority>`+
`<c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>`+
`</presence>`)
await u.waitUntil(() => modal.el.getAttribute('aria-hidden') === "true");
await u.waitUntil(() => !u.isVisible(modal.el));
await u.waitUntil(() => modal.getAttribute('aria-hidden') === "true");
await u.waitUntil(() => !u.isVisible(modal));
cbview.querySelector('.change-status').click()
modal = _converse.api.modal.get('modal-status-change');
await u.waitUntil(() => modal.el.getAttribute('aria-hidden') === "false", 1000);
modal.el.querySelector('label[for="radio-busy"]').click(); // Change status to "dnd"
modal.el.querySelector('[type="submit"]').click();
modal = _converse.api.modal.get('converse-chat-status-modal');
await u.waitUntil(() => modal.getAttribute('aria-hidden') === "false", 1000);
modal.querySelector('label[for="radio-busy"]').click(); // Change status to "dnd"
modal.querySelector('[type="submit"]').click();
await u.waitUntil(() => sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).length === 2);
sent_presence = sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).pop();
expect(Strophe.serialize(sent_presence))

View File

@ -55,12 +55,12 @@ describe("The Protocol", function () {
spyOn(_converse.api.vcard, "get").and.callThrough();
cbview.querySelector('.add-contact').click()
const modal = _converse.api.modal.get('add-contact-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
const modal = _converse.api.modal.get('converse-add-contact-modal');
await u.waitUntil(() => u.isVisible(modal), 1000);
modal.delegateEvents();
// Fill in the form and submit
const form = modal.el.querySelector('form.add-xmpp-contact');
const form = modal.querySelector('form.add-xmpp-contact');
form.querySelector('input[name="jid"]').value = 'contact@example.org';
form.querySelector('input[name="name"]').value = 'Chris Contact';
form.querySelector('input[name="group"]').value = 'My Buddies';

View File

@ -1,5 +1,19 @@
import log from "@converse/headless/log";
import { __ } from 'i18n';
import { _converse, api } from "@converse/headless/core";
export function removeContact (contact) {
contact.removeFromRoster(
() => contact.destroy(),
(e) => {
e && log.error(e);
api.alert('error', __('Error'), [
__('Sorry, there was an error while trying to remove %1$s as a contact.',
contact.getDisplayName())
]);
}
);
}
export function highlightRosterItem (chatbox) {
_converse.roster?.get(chatbox.get('jid'))?.trigger('highlight');

View File

@ -55,6 +55,7 @@ export default class AutoCompleteComponent extends CustomElement {
'name': { type: String },
'placeholder': { type: String },
'triggers': { type: String },
'required': { type: Boolean },
};
}
@ -78,6 +79,7 @@ export default class AutoCompleteComponent extends CustomElement {
<ul class="suggestion-box__results ${position_class}" hidden=""></ul>
<input
?autofocus=${this.autofocus}
?required=${this.required}
type="text"
name="${this.name}"
autocomplete="off"

View File

@ -1,5 +1,5 @@
import 'shared/registry.js';
import ImageModal from 'modals/image.js';
import ImageModal from 'shared/modals/image.js';
import renderRichText from 'shared/directives/rich-text.js';
import { CustomElement } from 'shared/components/element.js';
import { api } from "@converse/headless/core";
@ -31,7 +31,7 @@ export default class MessageBody extends CustomElement {
onImgClick (ev) { // eslint-disable-line class-methods-use-this
ev.preventDefault();
api.modal.create(ImageModal, {'src': ev.target.src}, ev).show(ev);
api.modal.show('converse-image-modal', {'src': ev.target.src}, ev);
}
onImgLoad () {

View File

@ -1,11 +1,11 @@
import './message-actions.js';
import './message-body.js';
import 'shared/components/dropdown.js';
import 'shared/modals/message-versions.js';
import 'shared/modals/user-details.js';
import 'shared/registry';
import 'plugins/muc-views/modals/occupant.js';
import tpl_file_progress from './templates/file-progress.js';
import MessageVersionsModal from 'modals/message-versions.js';
import OccupantModal from 'plugins/muc-views/modals/occupant.js';
import UserDetailsModal from 'modals/user-details.js';
import log from '@converse/headless/log';
import tpl_info_message from './templates/info-message.js';
import tpl_mep_message from 'plugins/muc-views/templates/mep-message.js';
@ -214,20 +214,20 @@ export default class Message extends CustomElement {
showUserModal (ev) {
if (this.model.get('sender') === 'me') {
api.modal.show(_converse.ProfileModal, {model: this.model}, ev);
api.modal.show('converse-profile-modal', {model: this.model}, ev);
} else if (this.model.get('type') === 'groupchat') {
ev.preventDefault();
api.modal.show(OccupantModal, { 'model': this.model.occupant, 'message': this.model }, ev);
api.modal.show('converse-muc-occupant-modal', { 'model': this.model.occupant, 'message': this.model }, ev);
} else {
ev.preventDefault();
const chatbox = this.model.collection.chatbox;
api.modal.show(UserDetailsModal, { model: chatbox }, ev);
api.modal.show('converse-user-details-modal', { model: chatbox }, ev);
}
}
showMessageVersionsModal (ev) {
ev.preventDefault();
api.modal.show(MessageVersionsModal, {'model': this.model}, ev);
api.modal.show('converse-message-versions-modal', {'model': this.model}, ev);
}
toggleSpoilerMessage (ev) {

View File

@ -0,0 +1,22 @@
import BaseModal from "plugins/modal/modal.js";
import tpl_image_modal from "./templates/image.js";
import { __ } from 'i18n';
import { api } from "@converse/headless/core";
import { getFileName } from 'utils/html.js';
import { html } from "lit";
import './styles/image.scss';
export default class ImageModal extends BaseModal {
renderModal () {
return tpl_image_modal({ 'src': this.src });
}
getModalTitle () {
return html`${__('Image: ')}<a target="_blank" rel="noopener" href="${this.src}">${getFileName(this.src)}</a>`;
}
}
api.elements.define('converse-image-modal', ImageModal);

View File

@ -0,0 +1,19 @@
import 'shared/components/message-versions.js';
import BaseModal from "plugins/modal/modal.js";
import { __ } from 'i18n';
import { html } from "lit";
import {api } from "@converse/headless/core";
export default class MessageVersionsModal extends BaseModal {
renderModal () {
return html`<converse-message-versions .model=${this.model}></converse-message-versions>`;
}
getModalTitle () { // eslint-disable-line class-methods-use-this
return __('Message versions');
}
}
api.elements.define('converse-message-versions-modal', MessageVersionsModal);

View File

@ -0,0 +1,6 @@
converse-image-modal {
.chat-image--modal {
max-height: 99%;
max-width: 100%;
}
}

View File

@ -0,0 +1,3 @@
import { html } from "lit";
export default (o) => html`<img class="chat-image chat-image--modal" src="${o.src}">`;

View File

@ -0,0 +1,69 @@
import avatar from 'shared/avatar/templates/avatar.js';
import { __ } from 'i18n';
import { html } from 'lit';
import { api } from "@converse/headless/core";
const remove_button = (el) => {
const i18n_remove_contact = __('Remove as contact');
return html`
<button type="button" @click="${ev => el.removeContact(ev)}" class="btn btn-danger remove-contact">
<converse-icon
class="fas fa-trash-alt"
color="var(--text-color-lighten-15-percent)"
size="1em"
></converse-icon>
${i18n_remove_contact}
</button>
`;
}
export const tpl_footer = (el) => {
const is_roster_contact = el.model.contact !== undefined;
const i18n_refresh = __('Refresh');
const allow_contact_removal = api.settings.get('allow_contact_removal');
return html`
<button type="button" class="btn btn-info refresh-contact" @click=${ev => el.refreshContact(ev)}>
<converse-icon
class="fa fa-refresh"
color="var(--text-color-lighten-15-percent)"
size="1em"
></converse-icon>
${i18n_refresh}</button>
${ (allow_contact_removal && is_roster_contact) ? remove_button(el) : '' }
`;
}
export const tpl_user_details_modal = (el) => {
const vcard = el.model?.vcard;
const vcard_json = vcard ? vcard.toJSON() : {};
const o = { ...el.model.toJSON(), ...vcard_json };
const i18n_address = __('XMPP Address');
const i18n_email = __('Email');
const i18n_full_name = __('Full Name');
const i18n_nickname = __('Nickname');
const i18n_profile = __('The User\'s Profile Image');
const i18n_role = __('Role');
const i18n_url = __('URL');
const avatar_data = {
'alt_text': i18n_profile,
'extra_classes': 'mb-3',
'height': '120',
'width': '120'
}
return html`
<div class="modal-body">
${ o.image ? html`<div class="mb-4">${avatar(Object.assign(o, avatar_data))}</div>` : '' }
${ 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>` : '' }
<converse-omemo-fingerprints jid=${o.jid}></converse-omemo-fingerprints>
</div>
`;
}

View File

@ -17,19 +17,19 @@ describe("The User Details Modal", function () {
const view = _converse.chatboxviews.get(contact_jid);
let show_modal_button = view.querySelector('.show-user-details-modal');
show_modal_button.click();
const modal = _converse.api.modal.get('user-details-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
const modal = _converse.api.modal.get('converse-user-details-modal');
await u.waitUntil(() => u.isVisible(modal), 1000);
spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true));
spyOn(view.model.contact, 'removeFromRoster').and.callFake(callback => callback());
let remove_contact_button = modal.el.querySelector('button.remove-contact');
let remove_contact_button = modal.querySelector('button.remove-contact');
expect(u.isVisible(remove_contact_button)).toBeTruthy();
remove_contact_button.click();
await u.waitUntil(() => modal.el.getAttribute('aria-hidden'), 1000);
await u.waitUntil(() => !u.isVisible(modal.el));
await u.waitUntil(() => modal.getAttribute('aria-hidden'), 1000);
await u.waitUntil(() => !u.isVisible(modal));
show_modal_button = view.querySelector('.show-user-details-modal');
show_modal_button.click();
remove_contact_button = modal.el.querySelector('button.remove-contact');
remove_contact_button = modal.querySelector('button.remove-contact');
expect(remove_contact_button === null).toBeTruthy();
}));
@ -44,15 +44,15 @@ describe("The User Details Modal", function () {
const view = _converse.chatboxviews.get(contact_jid);
let show_modal_button = view.querySelector('.show-user-details-modal');
show_modal_button.click();
let modal = _converse.api.modal.get('user-details-modal');
await u.waitUntil(() => u.isVisible(modal.el), 2000);
let modal = _converse.api.modal.get('converse-user-details-modal');
await u.waitUntil(() => u.isVisible(modal), 2000);
spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true));
spyOn(view.model.contact, 'removeFromRoster').and.callFake((callback, errback) => errback());
let remove_contact_button = modal.el.querySelector('button.remove-contact');
let remove_contact_button = modal.querySelector('button.remove-contact');
expect(u.isVisible(remove_contact_button)).toBeTruthy();
remove_contact_button.click();
await u.waitUntil(() => !u.isVisible(modal.el))
await u.waitUntil(() => !u.isVisible(modal))
await u.waitUntil(() => u.isVisible(document.querySelector('.alert-danger')), 2000);
const header = document.querySelector('.alert-danger .modal-title');
@ -62,14 +62,14 @@ describe("The User Details Modal", function () {
document.querySelector('.alert-danger button.close').click();
show_modal_button = view.querySelector('.show-user-details-modal');
show_modal_button.click();
modal = _converse.api.modal.get('user-details-modal');
await u.waitUntil(() => u.isVisible(modal.el), 2000)
modal = _converse.api.modal.get('converse-user-details-modal');
await u.waitUntil(() => u.isVisible(modal), 2000)
show_modal_button = view.querySelector('.show-user-details-modal');
show_modal_button.click();
await u.waitUntil(() => u.isVisible(modal.el), 2000)
await u.waitUntil(() => u.isVisible(modal), 2000)
remove_contact_button = modal.el.querySelector('button.remove-contact');
remove_contact_button = modal.querySelector('button.remove-contact');
expect(u.isVisible(remove_contact_button)).toBeTruthy();
}));
});

View File

@ -1,36 +1,17 @@
import BootstrapModal from "plugins/modal/base.js";
import BaseModal from "plugins/modal/modal.js";
import log from "@converse/headless/log";
import tpl_user_details_modal from "./templates/user-details.js";
import { tpl_user_details_modal, tpl_footer } from "./templates/user-details.js";
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
import { api, converse } from "@converse/headless/core";
import { removeContact } from 'plugins/rosterview/utils.js';
const u = converse.env.utils;
function removeContact (contact) {
contact.removeFromRoster(
() => contact.destroy(),
(e) => {
e && log.error(e);
api.alert('error', __('Error'), [
__('Sorry, there was an error while trying to remove %1$s as a contact.',
contact.getDisplayName())
]);
}
);
}
const UserDetailsModal = BootstrapModal.extend({
id: 'user-details-modal',
persistent: true,
events: {
'click button.refresh-contact': 'refreshContact',
},
export default class UserDetailsModal extends BaseModal {
initialize () {
BootstrapModal.prototype.initialize.apply(this, arguments);
super.initialize();
this.model.rosterContactAdded.then(() => this.registerContactEventHandlers());
this.listenTo(this.model, 'change', this.render);
this.registerContactEventHandlers();
@ -41,23 +22,19 @@ const UserDetailsModal = BootstrapModal.extend({
* @example _converse.api.listen.on('userDetailsModalInitialized', (chatbox) => { ... });
*/
api.trigger('userDetailsModalInitialized', this.model);
},
}
toHTML () {
const vcard = this.model?.vcard;
const vcard_json = vcard ? vcard.toJSON() : {};
return tpl_user_details_modal(Object.assign(
this.model.toJSON(),
vcard_json, {
'_converse': _converse,
'allow_contact_removal': api.settings.get('allow_contact_removal'),
'display_name': this.model.getDisplayName(),
'is_roster_contact': this.model.contact !== undefined,
'removeContact': ev => this.removeContact(ev),
'view': this,
'utils': u
}));
},
renderModal () {
return tpl_user_details_modal(this);
}
renderModalFooter () {
return tpl_footer(this);
}
getModalTitle () {
return this.model.getDisplayName();
}
registerContactEventHandlers () {
if (this.model.contact !== undefined) {
@ -68,7 +45,7 @@ const UserDetailsModal = BootstrapModal.extend({
this.render();
});
}
},
}
async refreshContact (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
@ -81,7 +58,7 @@ const UserDetailsModal = BootstrapModal.extend({
this.alert(__('Sorry, something went wrong while trying to refresh'), 'danger');
}
u.removeClass('fa-spin', refresh_icon);
},
}
async removeContact (ev) {
ev?.preventDefault?.();
@ -94,9 +71,7 @@ const UserDetailsModal = BootstrapModal.extend({
setTimeout(() => removeContact(this.model.contact), 1);
this.modal.hide();
}
},
});
}
}
_converse.UserDetailsModal = UserDetailsModal;
export default UserDetailsModal;
api.elements.define('converse-user-details-modal', UserDetailsModal);

View File

@ -140,13 +140,13 @@ async function openChatRoomViaModal (_converse, jid, nick='') {
await openControlBox(_converse);
document.querySelector('converse-rooms-list .show-add-muc-modal').click();
closeControlBox(_converse);
const modal = _converse.api.modal.get('add-chatroom-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1500)
modal.el.querySelector('input[name="chatroom"]').value = jid;
const modal = _converse.api.modal.get('converse-add-muc-modal');
await u.waitUntil(() => u.isVisible(modal), 1500)
modal.querySelector('input[name="chatroom"]').value = jid;
if (nick) {
modal.el.querySelector('input[name="nickname"]').value = nick;
modal.querySelector('input[name="nickname"]').value = nick;
}
modal.el.querySelector('form input[type="submit"]').click();
modal.querySelector('form input[type="submit"]').click();
await u.waitUntil(() => _converse.chatboxviews.get(jid), 1000);
return _converse.chatboxviews.get(jid);
}

View File

@ -74,7 +74,8 @@ function slideOutWrapup (el) {
el.style.height = '';
}
function getFileName (uri) {
export function getFileName (url) {
const uri = getURI(url);
try {
return decodeURI(uri.filename());
} catch (error) {

View File

@ -9,6 +9,7 @@
<script src="3rdparty/libsignal-protocol.js"></script>
<link rel="manifest" href="./manifest.json">
<link rel="shortcut icon" type="image/ico" href="favicon.ico"/>
<script src="https://cdn.conversejs.org/3rdparty/libsignal-protocol.min.js"></script>
</head>
<body class="reset"></body>
<script>