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/register/tests/register.js", type: 'module' },
{ pattern: "src/plugins/roomslist/tests/roomslist.js", type: 'module' }, { pattern: "src/plugins/roomslist/tests/roomslist.js", type: 'module' },
{ pattern: "src/plugins/rootview/tests/root.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/presence.js", type: 'module' },
{ pattern: "src/plugins/rosterview/tests/protocol.js", type: 'module' }, { pattern: "src/plugins/rosterview/tests/protocol.js", type: 'module' },
{ pattern: "src/plugins/rosterview/tests/roster.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": "^5.0.0",
"karma-jasmine-html-reporter": "^1.7.0", "karma-jasmine-html-reporter": "^1.7.0",
"karma-webpack": "^5.0.0", "karma-webpack": "^5.0.0",
"lerna": "^5.1.8", "lerna": "^5.5.1",
"mini-css-extract-plugin": "^2.6.0", "mini-css-extract-plugin": "^2.6.0",
"minimist": "^1.2.6", "minimist": "^1.2.6",
"po-loader": "0.6.1", "po-loader": "0.6.1",

View File

@ -1,6 +1,5 @@
import RosterContact from './contact.js'; import RosterContact from './contact.js';
import log from "@converse/headless/log"; import log from "@converse/headless/log";
import sum from 'lodash-es/sum';
import { Collection } from "@converse/skeletor/src/collection"; import { Collection } from "@converse/skeletor/src/collection";
import { Model } from "@converse/skeletor/src/model"; import { Model } from "@converse/skeletor/src/model";
import { _converse, api, converse } from "@converse/headless/core"; 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'; import { _converse, api, converse } from '@converse/headless/core';
const { u } = converse.env; const { u } = converse.env;
@ -34,6 +34,6 @@ export const bookmarkableChatRoomView = {
showBookmarkModal(ev) { showBookmarkModal(ev) {
ev?.preventDefault(); ev?.preventDefault();
const jid = this.model.get('jid'); 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 './form.js';
import BaseModal from "plugins/modal/base.js"; import BaseModal from "plugins/modal/modal.js";
import tpl_modal from './templates/modal.js'; import { html } from "lit";
import { __ } from 'i18n';
import { api } from "@converse/headless/core";
const MUCBookmarkFormModal = BaseModal.extend({ export default class BookmarkFormModal extends BaseModal {
id: "converse-bookmark-modal",
initialize (attrs) { renderModal () {
this.jid = attrs.jid; return html`
this.affiliation = attrs.affiliation; <converse-muc-bookmark-form class="muc-form-container" jid="${this.jid}">
BaseModal.prototype.initialize.apply(this, arguments); </converse-muc-bookmark-form>`;
},
toHTML () {
return tpl_modal(this);
} }
});
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'); expect(toggle.title).toBe('Bookmark this groupchat');
toggle.click(); toggle.click();
const modal = _converse.api.modal.get('converse-bookmark-modal'); const modal = _converse.api.modal.get('converse-bookmark-form-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000); await u.waitUntil(() => u.isVisible(modal), 1000);
/* Client uploads data: /* Client uploads data:
* -------------------- * --------------------
@ -66,13 +66,13 @@ describe("A chat room", function () {
* </iq> * </iq>
*/ */
expect(view.model.get('bookmarked')).toBeFalsy(); 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="name"]').value = 'Play&apos;s the Thing';
form.querySelector('input[name="autojoin"]').checked = 'checked'; form.querySelector('input[name="autojoin"]').checked = 'checked';
form.querySelector('input[name="nick"]').value = 'JC'; form.querySelector('input[name="nick"]').value = 'JC';
const IQ_stanzas = _converse.connection.IQ_stanzas; 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( const sent_stanza = await u.waitUntil(
() => IQ_stanzas.filter(s => sizzle('iq publish[node="storage:bookmarks"]', s).length).pop()); () => 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(); bookmark_icon.click();
expect(view.showBookmarkModal).toHaveBeenCalled(); expect(view.showBookmarkModal).toHaveBeenCalled();
const modal = _converse.api.modal.get('converse-bookmark-modal'); const modal = _converse.api.modal.get('converse-bookmark-form-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000); await u.waitUntil(() => u.isVisible(modal), 1000);
const form = await u.waitUntil(() => modal.el.querySelector('.chatroom-form')); const form = await u.waitUntil(() => modal.querySelector('.chatroom-form'));
expect(form.querySelector('input[name="name"]').value).toBe('The Play'); expect(form.querySelector('input[name="name"]').value).toBe('The Play');
expect(form.querySelector('input[name="autojoin"]').checked).toBeFalsy(); expect(form.querySelector('input[name="autojoin"]').checked).toBeFalsy();
expect(form.querySelector('input[name="nick"]').value).toBe('Othello'); expect(form.querySelector('input[name="nick"]').value).toBe('Othello');
// Remove the bookmark // 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); await u.waitUntil(() => view.querySelector('.chatbox-title__text .fa-bookmark') === null);
expect(_converse.bookmarks.length).toBe(0); 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 invokeMap from 'lodash-es/invokeMap';
import { Model } from '@converse/skeletor/src/model.js'; import { Model } from '@converse/skeletor/src/model.js';
import { __ } from 'i18n'; import { __ } from 'i18n';
@ -38,7 +38,7 @@ export async function removeBookmarkViaEvent (ev) {
export function addBookmarkViaEvent (ev) { export function addBookmarkViaEvent (ev) {
ev.preventDefault(); ev.preventDefault();
const jid = ev.currentTarget.getAttribute('data-room-jid'); 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 tpl_chatbox_head from './templates/chat-head.js';
import { CustomElement } from 'shared/components/element.js'; import { CustomElement } from 'shared/components/element.js';
import { __ } from 'i18n'; import { __ } from 'i18n';
@ -39,7 +39,7 @@ export default class ChatHeading extends CustomElement {
showUserDetailsModal (ev) { showUserDetailsModal (ev) {
ev.preventDefault(); ev.preventDefault();
api.modal.show(UserDetailsModal, { model: this.model }, ev); api.modal.show('converse-user-details-modal', { model: this.model }, ev);
} }
close (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); expect(view.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
view.querySelector('.chat-msg__content .fa-edit').click(); view.querySelector('.chat-msg__content .fa-edit').click();
const modal = _converse.api.modal.get('message-versions-modal'); const modal = _converse.api.modal.get('converse-message-versions-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000); await u.waitUntil(() => u.isVisible(modal), 1000);
const older_msgs = modal.el.querySelectorAll('.older-msg'); const older_msgs = modal.querySelectorAll('.older-msg');
expect(older_msgs.length).toBe(2); 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[0].textContent.includes('But soft, what light through yonder airlock breaks?')).toBe(true);
expect(view.model.messages.models.length).toBe(1); expect(view.model.messages.models.length).toBe(1);

View File

@ -3,8 +3,6 @@
const $msg = converse.env.$msg; const $msg = converse.env.$msg;
const u = converse.env.utils; const u = converse.env.utils;
const Strophe = converse.env.Strophe; const Strophe = converse.env.Strophe;
const sizzle = converse.env.sizzle;
describe("The Controlbox", function () { describe("The Controlbox", function () {
@ -124,10 +122,10 @@ describe("The Controlbox", function () {
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
var cbview = _converse.chatboxviews.get('controlbox'); var cbview = _converse.chatboxviews.get('controlbox');
cbview.querySelector('.change-status').click() 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);
modal.el.querySelector('label[for="radio-busy"]').click(); // Change status to "dnd" modal.querySelector('label[for="radio-busy"]').click(); // Change status to "dnd"
modal.el.querySelector('[type="submit"]').click(); modal.querySelector('[type="submit"]').click();
const sent_stanzas = _converse.connection.sent_stanzas; const sent_stanzas = _converse.connection.sent_stanzas;
const sent_presence = await u.waitUntil(() => sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).pop()); const sent_presence = await u.waitUntil(() => sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).pop());
expect(Strophe.serialize(sent_presence)).toBe( expect(Strophe.serialize(sent_presence)).toBe(
@ -149,12 +147,12 @@ describe("The Controlbox", function () {
await mock.openControlBox(_converse); await mock.openControlBox(_converse);
const cbview = _converse.chatboxviews.get('controlbox'); const cbview = _converse.chatboxviews.get('controlbox');
cbview.querySelector('.change-status').click() 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'; const msg = 'I am happy';
modal.el.querySelector('input[name="status_message"]').value = msg; modal.querySelector('input[name="status_message"]').value = msg;
modal.el.querySelector('[type="submit"]').click(); modal.querySelector('[type="submit"]').click();
const sent_stanzas = _converse.connection.sent_stanzas; const sent_stanzas = _converse.connection.sent_stanzas;
const sent_presence = await u.waitUntil(() => sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).pop()); const sent_presence = await u.waitUntil(() => sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).pop());
expect(Strophe.serialize(sent_presence)).toBe( 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 tpl_alert_modal from "./templates/alert.js";
import { __ } from 'i18n'; import { api } from "@converse/headless/core";
const Alert = BootstrapModal.extend({ export default class Alert extends BaseModal {
id: 'alert-modal',
initialize () { initialize () {
BootstrapModal.prototype.initialize.apply(this, arguments); super.initialize();
this.listenTo(this.model, 'change', this.render) this.listenTo(this.model, 'change', () => this.render())
}, this.addEventListener('hide.bs.modal', () => this.remove(), false);
toHTML () {
return tpl_alert_modal(Object.assign({__}, this.model.toJSON()));
} }
});
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 Confirm from './confirm.js';
import { Model } from '@converse/skeletor/src/model.js'; import { Model } from '@converse/skeletor/src/model.js';
let modals = []; let modals = [];
let modals_map = {};
const modal_api = { const modal_api = {
/** /**
@ -17,13 +17,20 @@ const modal_api = {
* Will create a new instance of that class if an existing one isn't * Will create a new instance of that class if an existing one isn't
* found. * found.
* @param { Class } ModalClass * @param { Class } ModalClass
* @param { Object } [properties] - Optional properties that will be * @param { Object } [properties] - Optional properties that will be set on a newly created modal instance.
* set on a newly created modal instance (if no pre-existing modal was
* found).
* @param { Event } [event] - The DOM event that causes the modal to be shown. * @param { Event } [event] - The DOM event that causes the modal to be shown.
*/ */
show (ModalClass, properties, ev) { show (name, properties, ev) {
const modal = this.get(ModalClass.id) || this.create(ModalClass, properties); 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); modal.show(ev);
return modal; return modal;
}, },
@ -33,28 +40,44 @@ const modal_api = {
* @param { String } id * @param { String } id
*/ */
get (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. * Create a modal of the passed-in type.
* @param { Class } ModalClass * @param { String } name
* @param { Object } [properties] - Optional properties that will be * @param { Object } [properties] - Optional properties that will be
* set on the modal instance. * set on the modal instance.
*/ */
create (ModalClass, properties) { create (name, properties) {
const modal = new ModalClass(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); modals.push(modal);
}
return modal; return modal;
}, },
/** /**
* Remove a particular modal * Remove a particular modal
* @param { View } modal * @param { String } name
*/ */
remove (modal) { 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); modals = modals.filter(m => m !== modal);
modal.remove(); }
modal?.remove();
}, },
/** /**
@ -63,6 +86,7 @@ const modal_api = {
removeAll () { removeAll () {
modals.forEach(m => m.remove()); modals.forEach(m => m.remove());
modals = []; modals = [];
modals_map = {};
} }
}, },
@ -157,7 +181,7 @@ const modal_api = {
'level': level, 'level': level,
'type': 'alert' '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 bootstrap from "bootstrap.native";
import log from "@converse/headless/log"; 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 { View } from '@converse/skeletor/src/view.js';
import { api, converse } from "@converse/headless/core"; import { api, converse } from "@converse/headless/core";
import { render } from 'lit'; 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 tpl_prompt from "./templates/prompt.js";
import { getOpenPromise } from '@converse/openpromise'; import { getOpenPromise } from '@converse/openpromise';
import { api } from "@converse/headless/core";
export default class Confirm extends BaseModal {
const Confirm = BootstrapModal.extend({ constructor (options) {
id: 'confirm-modal', super(options);
events: { this.confirmation = getOpenPromise();
'submit .confirm': 'onConfimation' }
},
initialize () { initialize () {
this.confirmation = getOpenPromise(); super.initialize();
BootstrapModal.prototype.initialize.apply(this, arguments); this.listenTo(this.model, 'change', () => this.render())
this.listenTo(this.model, 'change', this.render) this.addEventListener('hide.bs.modal', () => {
this.el.addEventListener('closed.bs.modal', () => this.confirmation.reject(), false);
},
toHTML () {
return tpl_prompt(this.model.toJSON());
},
afterRender () {
if (!this.close_handler_registered) {
this.el.addEventListener('closed.bs.modal', () => {
if (!this.confirmation.isResolved) { if (!this.confirmation.isResolved) {
this.confirmation.reject() this.confirmation.reject()
} }
}, false); }, false);
this.close_handler_registered = true;
} }
},
renderModal () {
return tpl_prompt(this);
}
getModalTitle () {
this.model.get('title');
}
onConfimation (ev) { onConfimation (ev) {
ev.preventDefault(); ev.preventDefault();
@ -53,6 +50,6 @@ const Confirm = BootstrapModal.extend({
this.confirmation.resolve(fields); this.confirmation.resolve(fields);
this.modal.hide(); 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 { &.fit-content {
box-sizing: content-box; box-sizing: content-box;
@ -64,12 +63,6 @@
} }
} }
} }
.modal-body--image {
.chat-image {
max-height: 99%;
max-width: 100%;
}
}
.modal-footer { .modal-footer {
justify-content: flex-start; justify-content: flex-start;
} }
@ -103,43 +96,5 @@
.btn { .btn {
font-weight: normal; 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 { html } from "lit";
import { modal_header_close_button } from "./buttons.js"
export default (o) => html` 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"> <div class="modal-body">
<span class="modal-alert"></span> <span class="modal-alert"></span>
${ o.messages.map(message => html`<p>${message}</p>`) } ${ o.messages.map(message => html`<p>${message}</p>`) }
</div>
</div>
</div>`; </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> </div>
`; `;
export default (el) => {
export default (o) => html` return html`
<div class="modal-dialog" role="document"> <form class="converse-form converse-form--modal confirm" action="#" @submit=${ev => el.onConfimation(ev)}>
<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"> <div class="form-group">
${ o.messages.map(message => html`<p>${message}</p>`) } ${ el.model.get('messages')?.map(message => html`<p>${message}</p>`) }
</div> </div>
${ o.fields.map(f => tpl_field(f)) } ${ el.model.get('fields')?.map(f => tpl_field(f)) }
<div class="form-group"> <div class="form-group">
<button type="submit" class="btn btn-primary">${__('OK')}</button> <button type="submit" class="btn btn-primary">${__('OK')}</button>
<input type="button" class="btn btn-secondary" data-dismiss="modal" value="${__('Cancel')}"/> <input type="button" class="btn btn-secondary" data-dismiss="modal" value="${__('Cancel')}"/>
</div> </div>
</form> </form>`;
</div> }
</div>
</div>
`;

View File

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

View File

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

View File

@ -1,20 +1,24 @@
import '../modtools.js'; import '../modtools.js';
import BaseModal from "plugins/modal/base.js"; import BaseModal from "plugins/modal/modal.js";
import tpl_moderator_tools from './templates/moderator-tools.js'; import { __ } from 'i18n';
import { api } from "@converse/headless/core";
import { html } from 'lit';
const ModeratorToolsModal = BaseModal.extend({ export default class ModeratorToolsModal extends BaseModal {
id: "converse-modtools-modal",
persistent: true,
initialize (attrs) { constructor (options) {
this.jid = attrs.jid; super(options);
this.affiliation = attrs.affiliation; this.id = "converse-modtools-modal";
BaseModal.prototype.initialize.apply(this, arguments);
},
toHTML () {
return tpl_moderator_tools(this);
} }
});
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 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({ export default class MUCDetailsModal extends BaseModal {
id: "muc-details-modal",
initialize () { initialize () {
BaseModal.prototype.initialize.apply(this, arguments); super.initialize();
this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'change', () => this.render());
this.listenTo(this.model.features, 'change', this.render); this.listenTo(this.model.features, 'change', () => this.render());
this.listenTo(this.model.occupants, 'add', this.render); this.listenTo(this.model.occupants, 'add', () => this.render());
this.listenTo(this.model.occupants, 'change', this.render); this.listenTo(this.model.occupants, 'change', () => this.render());
}, }
toHTML () { renderModal () {
return tpl_muc_details(this.model); 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 '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 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; const u = converse.env.utils;
export default class MUCInviteModal extends BaseModal {
export default BaseModal.extend({
id: "muc-invite-modal",
initialize () { initialize () {
BaseModal.prototype.initialize.apply(this, arguments); super.initialize();
this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'change', () => this.render());
this.initInviteWidget(); }
},
renderModal () {
toHTML () { return tpl_muc_invite_modal(this);
return tpl_muc_invite_modal(Object.assign( }
this.model.toJSON(), {
'submitInviteForm': ev => this.submitInviteForm(ev) getModalTitle () { // eslint-disable-line class-methods-use-this
}) return __('Invite someone to this groupchat');
); }
},
getAutoCompleteList () { // eslint-disable-line class-methods-use-this
initInviteWidget () { return _converse.roster.map(i => ({'label': i.getDisplayName(), 'value': i.get('jid')}));
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
});
},
submitInviteForm (ev) { submitInviteForm (ev) {
ev.preventDefault(); ev.preventDefault();
// TODO: Add support for sending an invite to multiple JIDs // TODO: Add support for sending an invite to multiple JIDs
const data = new FormData(ev.target); const data = new FormData(ev.target);
const jid = data.get('invitee_jids'); const jid = data.get('invitee_jids')?.trim();
const reason = data.get('reason'); const reason = data.get('reason');
if (u.isValidJID(jid)) { if (u.isValidJID(jid)) {
// TODO: Create and use API here // TODO: Create and use API here
@ -49,4 +39,6 @@ export default BaseModal.extend({
this.model.set({'invalid_invite_jid': true}); 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 head from "lodash-es/head";
import log from "@converse/headless/log"; 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_description from "../templates/muc-description.js";
import tpl_muc_list from "../templates/muc-list.js";
import tpl_spinner from "templates/spinner.js"; import tpl_spinner from "templates/spinner.js";
import { __ } from 'i18n'; import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core"; import { _converse, api, converse } from "@converse/headless/core";
@ -65,28 +65,25 @@ function toggleRoomInfo (ev) {
} }
export default BootstrapModal.extend({ export default class MUCListModal extends BaseModal {
id: "muc-list-modal",
persistent: true,
initialize () { constructor (options) {
super(options);
this.items = []; this.items = [];
this.loading_items = false; 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:muc_domain', this.onDomainChange);
this.listenTo(this.model, 'change:feedback_text', () => this.render()); this.listenTo(this.model, 'change:feedback_text', () => this.render());
this.addEventListener('shown.bs.modal', () => api.settings.get('locked_muc_domain') && this.updateRoomsList());
this.el.addEventListener('shown.bs.modal', () => api.settings.get('locked_muc_domain')
? this.updateRoomsList()
: this.el.querySelector('input[name="server"]').focus()
);
this.model.save('feedback_text', ''); this.model.save('feedback_text', '');
}, }
toHTML () { renderModal () {
return tpl_muc_list( return tpl_muc_list(
Object.assign(this.model.toJSON(), { Object.assign(this.model.toJSON(), {
'show_form': !api.settings.get('locked_muc_domain'), 'show_form': !api.settings.get('locked_muc_domain'),
@ -98,7 +95,11 @@ export default BootstrapModal.extend({
'submitForm': ev => this.showRooms(ev), 'submitForm': ev => this.showRooms(ev),
'toggleRoomInfo': ev => this.toggleRoomInfo(ev) 'toggleRoomInfo': ev => this.toggleRoomInfo(ev)
})); }));
}, }
getModalTitle () { // eslint-disable-line class-methods-use-this
return __('Query for Groupchats');
}
openRoom (ev) { openRoom (ev) {
ev.preventDefault(); ev.preventDefault();
@ -106,16 +107,16 @@ export default BootstrapModal.extend({
const name = ev.target.getAttribute('data-room-name'); const name = ev.target.getAttribute('data-room-name');
this.modal.hide(); this.modal.hide();
api.rooms.open(jid, {'name': name}, true); api.rooms.open(jid, {'name': name}, true);
}, }
toggleRoomInfo (ev) { toggleRoomInfo (ev) { // eslint-disable-line
ev.preventDefault(); ev.preventDefault();
toggleRoomInfo(ev); toggleRoomInfo(ev);
}, }
onDomainChange () { onDomainChange () {
api.settings.get('auto_list_rooms') && this.updateRoomsList(); api.settings.get('auto_list_rooms') && this.updateRoomsList();
}, }
/** /**
* Handle the IQ stanza returned from the server, containing * Handle the IQ stanza returned from the server, containing
@ -136,7 +137,7 @@ export default BootstrapModal.extend({
} }
this.render(); this.render();
return true; return true;
}, }
/** /**
* Send an IQ stanza to the server asking for all groupchats * Send an IQ stanza to the server asking for all groupchats
@ -152,7 +153,7 @@ export default BootstrapModal.extend({
api.sendIQ(iq) api.sendIQ(iq)
.then(iq => this.onRoomsFound(iq)) .then(iq => this.onRoomsFound(iq))
.catch(() => this.onRoomsFound()) .catch(() => this.onRoomsFound())
}, }
showRooms (ev) { showRooms (ev) {
ev.preventDefault(); ev.preventDefault();
@ -162,13 +163,15 @@ export default BootstrapModal.extend({
const data = new FormData(ev.target); const data = new FormData(ev.target);
this.model.setDomain(data.get('server')); this.model.setDomain(data.get('server'));
this.updateRoomsList(); this.updateRoomsList();
}, }
setDomainFromEvent (ev) { setDomainFromEvent (ev) {
this.model.setDomain(ev.target.value); this.model.setDomain(ev.target.value);
}, }
setNick (ev) { setNick (ev) {
this.model.save({nick: ev.target.value}); 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/modal.js";
import BaseModal from "plugins/modal/base.js"; import { __ } from 'i18n';
import { api } from "@converse/headless/core.js";
import { html } from 'lit';
export default BaseModal.extend({ export default class MUCNicknameModal extends BaseModal {
id: 'change-nickname-modal',
initialize (attrs) { renderModal () {
this.model = attrs.model; return html`<converse-muc-nickname-form jid="${this.model.get('jid')}"></converse-muc-nickname-form>`;
BaseModal.prototype.initialize.apply(this, arguments); }
},
toHTML () { getModalTitle () { // eslint-disable-line class-methods-use-this
return tpl_nickname(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 tpl_occupant_modal from "./templates/occupant.js";
import { _converse, api } from "@converse/headless/core"; import { _converse, api } from "@converse/headless/core";
const OccupantModal = BaseModal.extend({ export default class OccupantModal extends BaseModal {
id: "muc-occupant",
initialize () { initialize () {
BaseModal.prototype.initialize.apply(this, arguments); super.initialize()
if (this.model) { const model = this.model ?? this.message;
this.listenTo(this.model, 'change', this.render); this.listenTo(model, 'change', () => this.render());
}
/** /**
* Triggered once the OccupantModal has been initialized * Triggered once the OccupantModal has been initialized
* @event _converse#occupantModalInitialized * @event _converse#occupantModalInitialized
@ -18,7 +16,7 @@ const OccupantModal = BaseModal.extend({
* @example _converse.api.listen.on('occupantModalInitialized', data); * @example _converse.api.listen.on('occupantModalInitialized', data);
*/ */
api.trigger('occupantModalInitialized', { 'model': this.model, 'message': this.message }); api.trigger('occupantModalInitialized', { 'model': this.model, 'message': this.message });
}, }
getVcard () { getVcard () {
const model = this.model ?? this.message; const model = this.model ?? this.message;
@ -27,22 +25,24 @@ const OccupantModal = BaseModal.extend({
} }
const jid = model?.get('jid') || model?.get('from'); const jid = model?.get('jid') || model?.get('from');
return jid ? _converse.vcards.get(jid) : null; return jid ? _converse.vcards.get(jid) : null;
}, }
toHTML () { renderModal () {
const model = this.model ?? this.message; const model = this.model ?? this.message;
const jid = model?.get('jid'); const jid = model?.get('jid');
const vcard = this.getVcard(); const vcard = this.getVcard();
const display_name = model?.getDisplayName();
const nick = model.get('nick'); const nick = model.get('nick');
const occupant_id = model.get('occupant_id'); const occupant_id = model.get('occupant_id');
const role = this.model?.get('role'); const role = this.model?.get('role');
const affiliation = this.model?.get('affiliation'); const affiliation = this.model?.get('affiliation');
const hats = this.model?.get('hats')?.length ? this.model.get('hats') : null; 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 { __ } from 'i18n';
import { html } from "lit"; import { html } from "lit";
import { modal_close_button, modal_header_close_button } from "plugins/modal/templates/buttons.js";
const subject = (o) => { const subject = (o) => {
@ -16,7 +15,6 @@ const subject = (o) => {
export default (model) => { export default (model) => {
const o = model.toJSON(); const o = model.toJSON();
const config = model.config.toJSON(); const config = model.config.toJSON();
const display_name = __('Groupchat info for %1$s', model.getDisplayName());
const features = model.features.toJSON(); const features = model.features.toJSON();
const num_occupants = model.occupants.filter(o => o.get('show') !== 'offline').length; const num_occupants = model.occupants.filter(o => o.get('show') !== 'offline').length;
@ -51,14 +49,6 @@ export default (model) => {
const i18n_temporary = __('Temporary'); const i18n_temporary = __('Temporary');
const i18n_temporary_help = __('This groupchat will disappear once the last person leaves'); const i18n_temporary_help = __('This groupchat will disappear once the last person leaves');
return html` 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>
<div class="modal-body">
<span class="modal-alert"></span>
<div class="room-info"> <div class="room-info">
<p class="room-info"><strong>${i18n_name}</strong>: ${o.name}</p> <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_address}</strong>: <converse-rich-text text="xmpp:${o.jid}?join"></converse-rich-text></p>
@ -75,7 +65,7 @@ export default (model) => {
${ 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.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.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.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.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.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.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.moderated ? html`<li class="feature" ><converse-icon size="1em" class="fa fa-gavel"></converse-icon>${i18n_moderated} - <em>${i18n_moderated_help}</em></li>` : '' }
@ -84,10 +74,5 @@ export default (model) => {
</ul> </ul>
</div> </div>
</p> </p>
</div>
</div>
<div class="modal-footer">${modal_close_button}</div>
</div>
</div>
`; `;
} }

View File

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

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,18 +1,10 @@
import 'shared/avatar/avatar.js'; import 'shared/avatar/avatar.js';
import { __ } from 'i18n'; import { __ } from 'i18n';
import { html } from "lit"; import { html } from "lit";
import { modal_close_button, modal_header_close_button } from "plugins/modal/templates/buttons.js"
export default (o) => { export default (o) => {
return html` 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="row">
<div class="col-auto"> <div class="col-auto">
<converse-avatar <converse-avatar
@ -44,11 +36,5 @@ export default (o) => {
</ul> </ul>
</div> </div>
</div> </div>
</div>
<div class="modal-footer">
${modal_close_button}
</div>
</div>
</div>
`; `;
} }

View File

@ -25,6 +25,7 @@ export default class ModeratorTools extends CustomElement {
muc: { type: Object, attribute: false }, muc: { type: Object, attribute: false },
role: { type: String }, role: { type: String },
roles_filter: { type: String, attribute: false }, roles_filter: { type: String, attribute: false },
tab: { type: String },
users_with_affiliation: { type: Array, attribute: false }, users_with_affiliation: { type: Array, attribute: false },
users_with_role: { type: Array, attribute: false }, users_with_role: { type: Array, attribute: false },
}; };
@ -32,6 +33,7 @@ export default class ModeratorTools extends CustomElement {
constructor () { constructor () {
super(); super();
this.tab = 'affiliations';
this.affiliation = ''; this.affiliation = '';
this.affiliations_filter = ''; this.affiliations_filter = '';
this.role = ''; 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)), 'queryable_roles': ROLES.filter(a => !api.settings.get('modtools_disable_query').includes(a)),
'roles_filter': this.roles_filter, 'roles_filter': this.roles_filter,
'switchTab': ev => this.switchTab(ev), 'switchTab': ev => this.switchTab(ev),
'tab': this.tab,
'toggleForm': ev => this.toggleForm(ev), 'toggleForm': ev => this.toggleForm(ev),
'users_with_affiliation': this.users_with_affiliation, 'users_with_affiliation': this.users_with_affiliation,
'users_with_role': this.users_with_role, '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 () { async onSearchAffiliationChange () {
if (!this.affiliation) { if (!this.affiliation) {
return; return;

View File

@ -1,4 +1,5 @@
#add-chatroom-modal { converse-add-muc-modal {
.add-chatroom {
converse-autocomplete { converse-autocomplete {
.suggestion-box__results--below { .suggestion-box__results--below {
height: 10em; height: 10em;
@ -10,3 +11,4 @@
} }
} }
} }
}

View File

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

View File

@ -1,8 +1,14 @@
#muc-details-modal { converse-muc-details-modal {
.features-list { .features-list {
margin-left: 1em; margin-left: 1em;
} }
.room-info {
strong {
color: var(--muc-color);
}
}
.chatroom-features { .chatroom-features {
width: 100%; width: 100%;
.features-list { .features-list {
@ -13,9 +19,8 @@
padding-right: 0; padding-right: 0;
font-size: 1em; font-size: 1em;
cursor: help; cursor: help;
.fa { converse-icon {
margin-right: 0.5em; 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"> <ul class="nav nav-pills justify-content-center">
<li role="presentation" class="nav-item"> <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>
<li role="presentation" class="nav-item"> <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> </li>
</ul> </ul>
`; `;
@ -178,12 +190,12 @@ export default (o) => {
const show_both_tabs = o.queryable_roles.length && o.queryable_affiliations.length; const show_both_tabs = o.queryable_roles.length && o.queryable_affiliations.length;
return html` return html`
${o.alert_message ? html`<div class="alert alert-${o.alert_type}" role="alert">${o.alert_message}</div>` : '' } ${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"> <div class="tab-content">
${ o.queryable_affiliations.length ? html` ${ 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}> <form class="converse-form query-affiliation" @submit=${o.queryAffiliation}>
<p class="helptext pb-3">${i18n_helptext_affiliation}</p> <p class="helptext pb-3">${i18n_helptext_affiliation}</p>
<div class="form-group"> <div class="form-group">
@ -225,7 +237,7 @@ export default (o) => {
</div>` : '' } </div>` : '' }
${ o.queryable_roles.length ? html` ${ 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}> <form class="converse-form query-role" @submit=${o.queryRole}>
<p class="helptext pb-3">${i18n_helptext_role}</p> <p class="helptext pb-3">${i18n_helptext_role}</p>
<div class="form-group"> <div class="form-group">

View File

@ -1,7 +1,6 @@
import { __ } from 'i18n'; import { __ } from 'i18n';
import { html } from "lit"; import { html } from "lit";
import { repeat } from 'lit/directives/repeat.js'; 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"; import spinner from "templates/spinner.js";
@ -14,6 +13,7 @@ const form = (o) => {
<div class="form-group"> <div class="form-group">
<label for="chatroom">${i18n_server_address}:</label> <label for="chatroom">${i18n_server_address}:</label>
<input type="text" <input type="text"
autofocus
@change=${o.setDomainFromEvent} @change=${o.setDomainFromEvent}
value="${o.muc_domain || ''}" value="${o.muc_domain || ''}"
required="required" required="required"
@ -51,25 +51,12 @@ const tpl_item = (o, item) => {
export default (o) => { export default (o) => {
const i18n_list_chatrooms = __('Query for Groupchats');
return html` 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) : '' } ${o.show_form ? form(o) : '' }
<ul class="available-chatrooms list-group"> <ul class="available-chatrooms list-group">
${ o.loading_items ? html`<li class="list-group-item"> ${spinner()} </li>` : '' } ${ 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>` : '' } ${ 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))} ${repeat(o.items, item => item.jid, item => tpl_item(o, item))}
</ul> </ul>
</div>
<div class="modal-footer">${modal_close_button}</div>
</div>
</div>
`; `;
} }

View File

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

View File

@ -60,9 +60,9 @@ describe("A Groupchat Message", function () {
expect(view.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1); expect(view.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
const edit = await u.waitUntil(() => view.querySelector('.chat-msg__content .fa-edit')); const edit = await u.waitUntil(() => view.querySelector('.chat-msg__content .fa-edit'));
edit.click(); edit.click();
const modal = _converse.api.modal.get('message-versions-modal'); const modal = _converse.api.modal.get('converse-message-versions-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000); await u.waitUntil(() => u.isVisible(modal), 1000);
const older_msgs = modal.el.querySelectorAll('.older-msg'); const older_msgs = modal.querySelectorAll('.older-msg');
expect(older_msgs.length).toBe(2); 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[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); 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); expect(view.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
const edit = await u.waitUntil(() => view.querySelector('.chat-msg__content .fa-edit')); const edit = await u.waitUntil(() => view.querySelector('.chat-msg__content .fa-edit'));
edit.click(); edit.click();
const modal = _converse.api.modal.get('message-versions-modal'); const modal = _converse.api.modal.get('converse-message-versions-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000); await u.waitUntil(() => u.isVisible(modal), 1000);
const older_msgs = modal.el.querySelectorAll('.older-msg'); const older_msgs = modal.querySelectorAll('.older-msg');
expect(older_msgs.length).toBe(2); 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[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); 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'); const message_form = view.querySelector('converse-muc-message-form');
message_form.onKeyDown(enter); message_form.onKeyDown(enter);
const modal = await u.waitUntil(() => _converse.api.modal.get('converse-modtools-modal')); 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; return modal;
} }
@ -37,18 +37,18 @@ describe("The groupchat moderator tool", function () {
await u.waitUntil(() => (view.model.occupants.length === 5), 1000); await u.waitUntil(() => (view.model.occupants.length === 5), 1000);
const modal = await openModtools(_converse, view); 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 // Clear so that we don't match older stanzas
_converse.connection.IQ_stanzas = []; _converse.connection.IQ_stanzas = [];
tab.click(); tab.click();
let select = modal.el.querySelector('.select-affiliation'); let select = modal.querySelector('.select-affiliation');
expect(select.value).toBe('owner'); expect(select.value).toBe('owner');
select.value = 'admin'; 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(); button.click();
await u.waitUntil(() => !modal.loading_users_with_affiliation); await u.waitUntil(() => !modal.loading_users_with_affiliation);
await u.waitUntil(() => modal.el.querySelectorAll('.list-group--users > li').length); await u.waitUntil(() => modal.querySelectorAll('.list-group--users > li').length);
let user_els = modal.el.querySelectorAll('.list-group--users > li'); let user_els = modal.querySelectorAll('.list-group--users > li');
expect(user_els.length).toBe(1); 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.active').textContent.trim()).toBe('JID: wiccarocks@shakespeare.lit');
expect(user_els[0].querySelector('.list-group-item:nth-child(2n)').textContent.trim()).toBe('Nickname: wiccan'); 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'; select.value = 'owner';
button.click(); button.click();
await u.waitUntil(() => !modal.loading_users_with_affiliation); 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);
user_els = modal.el.querySelectorAll('.list-group--users > li'); user_els = modal.querySelectorAll('.list-group--users > li');
expect(user_els.length).toBe(2); 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.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(2n)').textContent.trim()).toBe('Nickname: romeo');
@ -112,25 +112,25 @@ describe("The groupchat moderator tool", function () {
]; ];
await mock.returnMemberLists(_converse, muc_jid, members); await mock.returnMemberLists(_converse, muc_jid, members);
await u.waitUntil(() => view.model.occupants.pluck('affiliation').filter(o => o === 'owner').length === 1); 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'); expect(alert.textContent.trim()).toBe('Affiliation changed');
await u.waitUntil(() => modal.el.querySelectorAll('.list-group--users > li').length === 1); await u.waitUntil(() => modal.querySelectorAll('.list-group--users > li').length === 1);
user_els = modal.el.querySelectorAll('.list-group--users > li'); user_els = modal.querySelectorAll('.list-group--users > li');
expect(user_els.length).toBe(1); 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.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(2n)').textContent.trim()).toBe('Nickname: romeo');
expect(user_els[0].querySelector('.list-group-item:nth-child(3n) div').textContent.trim()).toBe('Affiliation: owner'); expect(user_els[0].querySelector('.list-group-item:nth-child(3n) div').textContent.trim()).toBe('Affiliation: owner');
tab = modal.el.querySelector('#roles-tab'); modal.querySelector('#roles-tab').click();
tab.click(); select = modal.querySelector('.select-role');
select = modal.el.querySelector('.select-role'); await u.waitUntil(() => u.isVisible(select));
expect(u.isVisible(select)).toBe(true);
expect(select.value).toBe('moderator'); 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(); 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); await u.waitUntil(() => roles_panel.querySelectorAll('.list-group--users > li').length === 1);
select.value = 'participant'; select.value = 'participant';
button.click(); button.click();
@ -158,35 +158,35 @@ describe("The groupchat moderator tool", function () {
// Clear so that we don't match older stanzas // Clear so that we don't match older stanzas
_converse.connection.IQ_stanzas = []; _converse.connection.IQ_stanzas = [];
const modal = await openModtools(_converse, view); const modal = await openModtools(_converse, view);
const select = modal.el.querySelector('.select-affiliation'); const select = modal.querySelector('.select-affiliation');
expect(select.value).toBe('owner'); expect(select.value).toBe('owner');
select.value = 'member'; 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(); button.click();
await u.waitUntil(() => !modal.loading_users_with_affiliation); 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'); 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); expect(filter).not.toBe(null);
filter.value = 'romeo'; filter.value = 'romeo';
u.triggerEvent(filter, "keyup", "KeyboardEvent"); 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'; filter.value = 'r';
u.triggerEvent(filter, "keyup", "KeyboardEvent"); 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'; filter.value = 'gower';
u.triggerEvent(filter, "keyup", "KeyboardEvent"); 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'; filter.value = 'RoMeO';
u.triggerEvent(filter, "keyup", "KeyboardEvent"); 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); message_form.onKeyDown(enter);
const modal = await u.waitUntil(() => _converse.api.modal.get('converse-modtools-modal')); 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(); tab.click();
// Clear so that we don't match older stanzas // Clear so that we don't match older stanzas
_converse.connection.IQ_stanzas = []; _converse.connection.IQ_stanzas = [];
const select = modal.el.querySelector('.select-role'); const select = modal.querySelector('.select-role');
expect(select.value).toBe('moderator'); expect(select.value).toBe('moderator');
select.value = 'participant'; 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(); button.click();
await u.waitUntil(() => !modal.loading_users_with_role); 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'); 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); expect(filter).not.toBe(null);
filter.value = 'tux'; filter.value = 'tux';
u.triggerEvent(filter, "keyup", "KeyboardEvent"); 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'; filter.value = 'r';
u.triggerEvent(filter, "keyup", "KeyboardEvent"); 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'; filter.value = 'crone';
u.triggerEvent(filter, "keyup", "KeyboardEvent"); 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", 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); const view = _converse.chatboxviews.get(muc_jid);
await u.waitUntil(() => (view.model.occupants.length === 5)); await u.waitUntil(() => (view.model.occupants.length === 5));
const modal = await openModtools(_converse, view); 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 // Clear so that we don't match older stanzas
_converse.connection.IQ_stanzas = []; _converse.connection.IQ_stanzas = [];
const IQ_stanzas = _converse.connection.IQ_stanzas; const IQ_stanzas = _converse.connection.IQ_stanzas;
tab.click(); tab.click();
const select = modal.el.querySelector('.select-affiliation'); const select = modal.querySelector('.select-affiliation');
select.value = 'outcast'; 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(); button.click();
const iq_query = await u.waitUntil(() => _.filter( const iq_query = await u.waitUntil(() => _.filter(
@ -338,10 +338,10 @@ describe("The groupchat moderator tool", function () {
_converse.connection._dataRecv(mock.createRequest(error)); _converse.connection._dataRecv(mock.createRequest(error));
await u.waitUntil(() => !modal.loading_users_with_affiliation); 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'); 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.length).toBe(1);
expect(user_els[0].textContent.trim()).toBe('No users with that affiliation found.'); 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 // Clear so that we don't match older stanzas
_converse.connection.IQ_stanzas = []; _converse.connection.IQ_stanzas = [];
const tab = modal.el.querySelector('#affiliations-tab'); const tab = modal.querySelector('#affiliations-tab');
tab.click(); tab.click();
const select = modal.el.querySelector('.select-affiliation'); const select = modal.querySelector('.select-affiliation');
select.value = 'member'; 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(); button.click();
await u.waitUntil(() => !modal.loading_users_with_affiliation); 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 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'); const form = user_els[0].querySelector('.list-group-item:nth-child(3n) .affiliation-form');
expect(u.hasClass('hidden', form)).toBeTruthy(); expect(u.hasClass('hidden', form)).toBeTruthy();
@ -422,19 +422,19 @@ describe("The groupchat moderator tool", function () {
const view = _converse.chatboxviews.get(muc_jid); const view = _converse.chatboxviews.get(muc_jid);
await u.waitUntil(() => (view.model.occupants.length === 3)); await u.waitUntil(() => (view.model.occupants.length === 3));
const modal = await openModtools(_converse, view); 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 // Clear so that we don't match older stanzas
_converse.connection.IQ_stanzas = []; _converse.connection.IQ_stanzas = [];
tab.click(); tab.click();
const show_affiliation_dropdown = modal.el.querySelector('.select-affiliation'); const show_affiliation_dropdown = modal.querySelector('.select-affiliation');
show_affiliation_dropdown.value = 'member'; 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(); button.click();
await u.waitUntil(() => !modal.loading_users_with_affiliation); 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'); 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']); 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'); const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
roomspanel.querySelector('.show-add-muc-modal').click(); roomspanel.querySelector('.show-add-muc-modal').click();
mock.closeControlBox(_converse); mock.closeControlBox(_converse);
const modal = _converse.api.modal.get('add-chatroom-modal'); const modal = _converse.api.modal.get('converse-add-muc-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000); 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:'); 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'); 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:'); 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(''); expect(nick_input.value).toBe('');
nick_input.value = 'romeo'; 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()); spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
modal.el.querySelector('input[name="chatroom"]').value = 'lounge@muc.montague.lit'; modal.querySelector('input[name="chatroom"]').value = 'lounge@muc.montague.lit';
modal.el.querySelector('form input[type="submit"]').click(); modal.querySelector('form input[type="submit"]').click();
await u.waitUntil(() => _converse.chatboxes.length); await u.waitUntil(() => _converse.chatboxes.length);
await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 1); await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 1);
roomspanel.model.set('muc_domain', 'muc.example.org'); roomspanel.model.set('muc_domain', 'muc.example.org');
roomspanel.querySelector('.show-add-muc-modal').click(); roomspanel.querySelector('.show-add-muc-modal').click();
label_name = modal.el.querySelector('label[for="chatroom"]'); label_name = modal.querySelector('label[for="chatroom"]');
expect(label_name.textContent.trim()).toBe('Groupchat address:'); expect(label_name.textContent.trim()).toBe('Groupchat name:');
await u.waitUntil(() => modal.el.querySelector('input[name="chatroom"]')?.placeholder === 'name@muc.example.org'); 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); await mock.openControlBox(_converse);
const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list'); const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
roomspanel.querySelector('.show-add-muc-modal').click(); roomspanel.querySelector('.show-add-muc-modal').click();
const modal = _converse.api.modal.get('add-chatroom-modal'); const modal = _converse.api.modal.get('converse-add-muc-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000); await u.waitUntil(() => u.isVisible(modal), 1000);
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()); 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:'); 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'); expect(name_input.placeholder).toBe('name@muc.example.org');
name_input.value = 'lounge'; 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'; 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(() => _converse.chatboxes.length);
await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 1); 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); 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 // However, you can still open MUCs with different domains
roomspanel.querySelector('.show-add-muc-modal').click(); roomspanel.querySelector('.show-add-muc-modal').click();
await u.waitUntil(() => u.isVisible(modal.el), 1000); await u.waitUntil(() => u.isVisible(modal), 1000);
name_input = modal.el.querySelector('input[name="chatroom"]'); name_input = modal.querySelector('input[name="chatroom"]');
name_input.value = 'lounge@conference.example.org'; 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'; 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(() => _converse.chatboxes.models.filter(c => c.get('type') === 'chatroom').length === 2);
await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).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( 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); await mock.openControlBox(_converse);
const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list'); const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
roomspanel.querySelector('.show-add-muc-modal').click(); roomspanel.querySelector('.show-add-muc-modal').click();
const modal = _converse.api.modal.get('add-chatroom-modal'); const modal = _converse.api.modal.get('converse-add-muc-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000); await u.waitUntil(() => u.isVisible(modal), 1000);
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()); 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:'); 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(''); expect(name_input.placeholder).toBe('');
name_input.value = 'lounge'; 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'; 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(() => _converse.chatboxes.length);
await u.waitUntil(() => sizzle('.chatroom', _converse.el).filter(u.isVisible).length === 1); 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); 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 // However, you can still open MUCs with different domains
roomspanel.querySelector('.show-add-muc-modal').click(); roomspanel.querySelector('.show-add-muc-modal').click();
await u.waitUntil(() => u.isVisible(modal.el), 1000); await u.waitUntil(() => u.isVisible(modal), 1000);
name_input = modal.el.querySelector('input[name="chatroom"]'); name_input = modal.querySelector('input[name="chatroom"]');
name_input.value = 'lounge@conference'; name_input.value = 'lounge@conference';
nick_input = modal.el.querySelector('input[name="nickname"]'); nick_input = modal.querySelector('input[name="nickname"]');
nick_input.value = 'max'; nick_input.value = 'max';
modal.el.querySelector('form input[type="submit"]').click(); modal.querySelector('form input[type="submit"]').click();
await u.waitUntil( await u.waitUntil(
() => _converse.chatboxes.models.filter(c => c.get('type') === 'chatroom').length === 2 () => _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'); const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
roomspanel.querySelector('.show-list-muc-modal').click(); roomspanel.querySelector('.show-list-muc-modal').click();
mock.closeControlBox(_converse); mock.closeControlBox(_converse);
const modal = _converse.api.modal.get('muc-list-modal'); const modal = _converse.api.modal.get('converse-muc-list-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000); await u.waitUntil(() => u.isVisible(modal), 1000);
spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve()); spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
// See: https://xmpp.org/extensions/xep-0045.html#disco-rooms // 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'); expect(server_input.placeholder).toBe('conference.example.org');
server_input.value = 'chat.shakespeare.lit'; 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); await u.waitUntil(() => _converse.chatboxes.length);
const IQ_stanzas = _converse.connection.IQ_stanzas; 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; .c('item', { jid: 'street@chat.shakespeare.lit', name: 'A street' }).nodeTree;
_converse.connection._dataRecv(mock.createRequest(iq)); _converse.connection._dataRecv(mock.createRequest(iq));
await u.waitUntil(() => modal.el.querySelectorAll('.available-chatrooms li').length === 11); await u.waitUntil(() => modal.querySelectorAll('.available-chatrooms li').length === 11);
const rooms = modal.el.querySelectorAll('.available-chatrooms li'); const rooms = modal.querySelectorAll('.available-chatrooms li');
expect(rooms[0].textContent.trim()).toBe('Groupchats found'); expect(rooms[0].textContent.trim()).toBe('Groupchats found');
expect(rooms[1].textContent.trim()).toBe('A Lonely Heath'); expect(rooms[1].textContent.trim()).toBe('A Lonely Heath');
expect(rooms[2].textContent.trim()).toBe('A Dark Cave'); 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'); const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
roomspanel.querySelector('.show-list-muc-modal').click(); roomspanel.querySelector('.show-list-muc-modal').click();
mock.closeControlBox(_converse); mock.closeControlBox(_converse);
const modal = _converse.api.modal.get('muc-list-modal'); const modal = _converse.api.modal.get('converse-muc-list-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000); await u.waitUntil(() => u.isVisible(modal), 1000);
const server_input = modal.el.querySelector('input[name="server"]'); const server_input = modal.querySelector('input[name="server"]');
expect(server_input.value).toBe('muc.example.org'); 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'); const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
roomspanel.querySelector('.show-list-muc-modal').click(); roomspanel.querySelector('.show-list-muc-modal').click();
mock.closeControlBox(_converse); mock.closeControlBox(_converse);
const modal = _converse.api.modal.get('muc-list-modal'); const modal = _converse.api.modal.get('converse-muc-list-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000); await u.waitUntil(() => u.isVisible(modal), 1000);
spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve()); spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
expect(modal.el.querySelector('input[name="server"]')).toBe(null); expect(modal.querySelector('input[name="server"]')).toBe(null);
expect(modal.el.querySelector('input[type="submit"]')).toBe(null); expect(modal.querySelector('input[type="submit"]')).toBe(null);
await u.waitUntil(() => _converse.chatboxes.length); await u.waitUntil(() => _converse.chatboxes.length);
const sent_stanza = await u.waitUntil(() => const sent_stanza = await u.waitUntil(() =>
_converse.connection.sent_stanzas _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(); .c('item', { jid: 'forres@chat.shakespeare.lit', name: 'The Palace' }).up();
_converse.connection._dataRecv(mock.createRequest(iq)); _converse.connection._dataRecv(mock.createRequest(iq));
await u.waitUntil(() => modal.el.querySelectorAll('.available-chatrooms li').length === 4); await u.waitUntil(() => modal.querySelectorAll('.available-chatrooms li').length === 4);
const rooms = modal.el.querySelectorAll('.available-chatrooms li'); const rooms = modal.querySelectorAll('.available-chatrooms li');
expect(rooms[0].textContent.trim()).toBe('Groupchats found'); expect(rooms[0].textContent.trim()).toBe('Groupchats found');
expect(rooms[1].textContent.trim()).toBe('A Lonely Heath'); expect(rooms[1].textContent.trim()).toBe('A Lonely Heath');
expect(rooms[2].textContent.trim()).toBe('A Dark Cave'); 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')); await u.waitUntil(() => view.querySelector('.open-invite-modal'));
view.querySelector('.open-invite-modal').click(); view.querySelector('.open-invite-modal').click();
const modal = _converse.api.modal.get('muc-invite-modal'); const modal = _converse.api.modal.get('converse-muc-invite-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000) await u.waitUntil(() => u.isVisible(modal), 1000)
expect(modal.el.querySelectorAll('#invitee_jids').length).toBe(1); expect(modal.querySelectorAll('#invitee_jids').length).toBe(1);
expect(modal.el.querySelectorAll('textarea').length).toBe(1); expect(modal.querySelectorAll('textarea').length).toBe(1);
spyOn(view.model, 'directInvite').and.callThrough(); spyOn(view.model, 'directInvite').and.callThrough();
const input = modal.el.querySelector('#invitee_jids'); const input = modal.querySelector('#invitee_jids input');
input.value = "Balt"; 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'); expect(error.textContent).toBe('Please enter a valid XMPP address');
let evt = new Event('input'); let evt = new Event('input');
@ -1354,7 +1354,7 @@ describe("Groupchats", function () {
let sent_stanza; let sent_stanza;
spyOn(_converse.connection, 'send').and.callFake(stanza => (sent_stanza = 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(input.value).toBe('Balt');
expect(hint.textContent.trim()).toBe('Balthasar'); expect(hint.textContent.trim()).toBe('Balthasar');
@ -1362,9 +1362,9 @@ describe("Groupchats", function () {
evt.button = 0; evt.button = 0;
hint.dispatchEvent(evt); hint.dispatchEvent(evt);
const textarea = modal.el.querySelector('textarea'); const textarea = modal.querySelector('textarea');
textarea.value = "Please join!"; textarea.value = "Please join!";
modal.el.querySelector('button[type="submit"]').click(); modal.querySelector('input[type="submit"]').click();
expect(view.model.directInvite).toHaveBeenCalled(); expect(view.model.directInvite).toHaveBeenCalled();
expect(Strophe.serialize(sent_stanza)).toBe( expect(Strophe.serialize(sent_stanza)).toBe(
@ -1634,10 +1634,10 @@ describe("Groupchats", function () {
const info_el = view.querySelector(".show-muc-details-modal"); const info_el = view.querySelector(".show-muc-details-modal");
info_el.click(); info_el.click();
let modal = _converse.api.modal.get('muc-details-modal'); let modal = _converse.api.modal.get('converse-muc-details-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000); 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); let features_shown = features_list.textContent.split('\n').map(s => s.trim()).filter(s => s);
expect(features_shown.join(' ')).toBe( expect(features_shown.join(' ')).toBe(
@ -1661,7 +1661,7 @@ describe("Groupchats", function () {
expect(view.model.features.get('unsecured')).toBe(false); expect(view.model.features.get('unsecured')).toBe(false);
await u.waitUntil(() => view.querySelector('.chatbox-title__text').textContent.trim() === 'Room'); 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(); view.querySelector('.configure-chatroom-button').click();
const IQs = _converse.connection.IQ_stanzas; 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))); await u.waitUntil(() => new Promise(success => view.model.features.on('change', success)));
info_el.click(); info_el.click();
modal = _converse.api.modal.get('muc-details-modal'); modal = _converse.api.modal.get('converse-muc-details-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000); 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); features_shown = features_list.textContent.split('\n').map(s => s.trim()).filter(s => s);
expect(features_shown.join(' ')).toBe( expect(features_shown.join(' ')).toBe(
'Password protected - This groupchat requires a password before entry '+ '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"); const dropdown_item = view.querySelector(".open-nickname-modal");
dropdown_item.click(); dropdown_item.click();
const modal = _converse.api.modal.get('change-nickname-modal'); const modal = _converse.api.modal.get('converse-muc-nickname-modal');
await u.waitUntil(() => u.isVisible(modal.el)); 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); expect(input.value).toBe(nick);
const newnick = 'loverboy'; const newnick = 'loverboy';
input.value = newnick; 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_stanzas } = _converse.connection;
const sent_stanza = sent_stanzas.pop() const sent_stanza = sent_stanzas.pop()
@ -422,13 +422,13 @@ describe("A MUC", function () {
const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list'); const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
roomspanel.querySelector('.show-add-muc-modal').click(); roomspanel.querySelector('.show-add-muc-modal').click();
mock.closeControlBox(_converse); mock.closeControlBox(_converse);
const modal = _converse.api.modal.get('add-chatroom-modal'); const modal = _converse.api.modal.get('converse-add-muc-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000) await u.waitUntil(() => u.isVisible(modal), 1000)
const name_input = modal.el.querySelector('input[name="chatroom"]'); const name_input = modal.querySelector('input[name="chatroom"]');
name_input.value = 'lounge@montague.lit'; name_input.value = 'lounge@montague.lit';
expect(modal.el.querySelector('label[for="nickname"]')).toBe(null); expect(modal.querySelector('label[for="nickname"]')).toBe(null);
expect(modal.el.querySelector('input[name="nickname"]')).toBe(null); expect(modal.querySelector('input[name="nickname"]')).toBe(null);
modal.el.querySelector('form input[type="submit"]').click(); modal.querySelector('form input[type="submit"]').click();
await u.waitUntil(() => _converse.chatboxes.length > 1); await u.waitUntil(() => _converse.chatboxes.length > 1);
const chatroom = _converse.chatboxes.get('lounge@montague.lit'); const chatroom = _converse.chatboxes.get('lounge@montague.lit');
expect(chatroom.get('nick')).toBe('romeo'); expect(chatroom.get('nick')).toBe('romeo');
@ -442,11 +442,11 @@ describe("A MUC", function () {
const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list'); const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
roomspanel.querySelector('.show-add-muc-modal').click(); roomspanel.querySelector('.show-add-muc-modal').click();
mock.closeControlBox(_converse); mock.closeControlBox(_converse);
const modal = _converse.api.modal.get('add-chatroom-modal'); const modal = _converse.api.modal.get('converse-add-muc-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000) await u.waitUntil(() => u.isVisible(modal), 1000)
const label_nick = modal.el.querySelector('label[for="nickname"]'); const label_nick = modal.querySelector('label[for="nickname"]');
expect(label_nick.textContent.trim()).toBe('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'); expect(nick_input.value).toBe('romeo');
})); }));
@ -458,11 +458,11 @@ describe("A MUC", function () {
const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list'); const roomspanel = _converse.chatboxviews.get('controlbox').querySelector('converse-rooms-list');
roomspanel.querySelector('.show-add-muc-modal').click(); roomspanel.querySelector('.show-add-muc-modal').click();
mock.closeControlBox(_converse); mock.closeControlBox(_converse);
const modal = _converse.api.modal.get('add-chatroom-modal'); const modal = _converse.api.modal.get('converse-add-muc-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000) await u.waitUntil(() => u.isVisible(modal), 1000)
const label_nick = modal.el.querySelector('label[for="nickname"]'); const label_nick = modal.querySelector('label[for="nickname"]');
expect(label_nick.textContent.trim()).toBe('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'); expect(nick_input.value).toBe('st.nick');
})); }));
}); });

View File

@ -1,5 +1,5 @@
import ModeratorToolsModal from './modals/moderator-tools.js'; import './modals/occupant.js';
import OccupantModal from './modals/occupant.js'; import './modals/moderator-tools.js';
import log from "@converse/headless/log"; import log from "@converse/headless/log";
import tpl_spinner from 'templates/spinner.js'; import tpl_spinner from 'templates/spinner.js';
import { __ } from 'i18n'; import { __ } from 'i18n';
@ -234,19 +234,19 @@ export function showModeratorToolsModal (muc, affiliation) {
if (!muc.verifyRoles(['moderator'])) { if (!muc.verifyRoles(['moderator'])) {
return; return;
} }
let modal = api.modal.get(ModeratorToolsModal.id); let modal = api.modal.get('converse-modtools-modal');
if (modal) { if (modal) {
modal.affiliation = affiliation; modal.affiliation = affiliation;
modal.render(); modal.render();
} else { } 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(); modal.show();
} }
export function showOccupantModal (ev, occupant) { 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 './fingerprints.js';
import './profile.js'; import './profile.js';
import 'modals/user-details.js'; import 'shared/modals/user-details.js';
import 'plugins/profile/index.js'; import 'plugins/profile/index.js';
import ConverseMixins from './mixins/converse.js'; import ConverseMixins from './mixins/converse.js';
import Device from './device.js'; import Device from './device.js';

View File

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

View File

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

View File

@ -3,11 +3,12 @@
* @license Mozilla Public License (MPLv2) * @license Mozilla Public License (MPLv2)
*/ */
import '../modal/index.js'; import '../modal/index.js';
import './modals/chat-status.js';
import './modals/profile.js';
import './modals/user-settings.js';
import './statusview.js'; import './statusview.js';
import '@converse/headless/plugins/status'; import '@converse/headless/plugins/status';
import '@converse/headless/plugins/vcard'; import '@converse/headless/plugins/vcard';
import './modals/chat-status.js';
import './modals/profile.js';
import { api, converse } from '@converse/headless/core'; 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 tpl_chat_status_modal from "../templates/chat-status-modal.js";
import { __ } from 'i18n'; import { __ } from 'i18n';
import { _converse, converse } from "@converse/headless/core"; import { _converse, api, converse } from "@converse/headless/core";
const u = converse.env.utils; const u = converse.env.utils;
const ChatStatusModal = BootstrapModal.extend({ export default class ChatStatusModal extends BaseModal {
id: "modal-status-change",
events: {
"submit form#set-xmpp-status": "onFormSubmitted",
"click .clear-input": "clearStatusMessage"
},
toHTML () { initialize () {
return tpl_chat_status_modal( super.initialize();
Object.assign( this.render();
this.model.toJSON(), this.addEventListener('shown.bs.modal', () => {
this.model.vcard.toJSON(), { this.querySelector('input[name="status_message"]').focus();
'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();
}, false); }, false);
}, }
renderModal () {
return tpl_chat_status_modal(this);
}
getModalTitle () { // eslint-disable-line class-methods-use-this
return __('Change chat status');
}
clearStatusMessage (ev) { clearStatusMessage (ev) {
if (ev && ev.preventDefault) { if (ev && ev.preventDefault) {
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 = ''; roster_filter.value = '';
}, }
onFormSubmitted (ev) { onFormSubmitted (ev) {
ev.preventDefault(); ev.preventDefault();
@ -56,9 +42,8 @@ const ChatStatusModal = BootstrapModal.extend({
}); });
this.modal.hide(); this.modal.hide();
} }
}); }
_converse.ChatStatusModal = ChatStatusModal; _converse.ChatStatusModal = ChatStatusModal;
export default ChatStatusModal; api.elements.define('converse-chat-status-modal', ChatStatusModal);

View File

@ -1,32 +1,27 @@
import BootstrapModal from "plugins/modal/base.js"; import BaseModal from "plugins/modal/modal.js";
import bootstrap from "bootstrap.native";
import log from "@converse/headless/log"; import log from "@converse/headless/log";
import tpl_profile_modal from "../templates/profile_modal.js"; import tpl_profile_modal from "../templates/profile_modal.js";
import Compress from 'client-compress'; import Compress from 'client-compress';
import { __ } from 'i18n'; 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({
const options = {
targetSize: 0.1, targetSize: 0.1,
quality: 0.75, quality: 0.75,
maxWidth: 256, maxWidth: 256,
maxHeight: 256 maxHeight: 256
});
export default class ProfileModal extends BaseModal {
constructor (options) {
super(options);
this.tab = 'profile';
} }
const compress = new Compress(options)
const ProfileModal = BootstrapModal.extend({
id: "user-profile-modal",
events: {
'submit .profile-form': 'onFormSubmitted'
},
initialize () { initialize () {
super.initialize();
this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'change', this.render);
BootstrapModal.prototype.initialize.apply(this, arguments);
/** /**
* Triggered when the _converse.ProfileModal has been created and initialized. * Triggered when the _converse.ProfileModal has been created and initialized.
* @event _converse#profileModalInitialized * @event _converse#profileModalInitialized
@ -34,19 +29,15 @@ const ProfileModal = BootstrapModal.extend({
* @example _converse.api.listen.on('profileModalInitialized', status => { ... }); * @example _converse.api.listen.on('profileModalInitialized', status => { ... });
*/ */
api.trigger('profileModalInitialized', this.model); api.trigger('profileModalInitialized', this.model);
}, }
toHTML () { renderModal () {
return tpl_profile_modal(Object.assign( return tpl_profile_modal(this);
this.model.toJSON(), }
this.model.vcard.toJSON(),
{ 'view': this }
));
},
afterRender () { getModalTitle () { // eslint-disable-line class-methods-use-this
this.tabs = sizzle('.nav-item .nav-link', this.el).map(e => new bootstrap.Tab(e)); return __('Your Profile');
}, }
async setVCard (data) { async setVCard (data) {
try { try {
@ -60,7 +51,7 @@ const ProfileModal = BootstrapModal.extend({
return; return;
} }
this.modal.hide(); this.modal.hide();
}, }
onFormSubmitted (ev) { onFormSubmitted (ev) {
ev.preventDefault(); ev.preventDefault();
@ -95,8 +86,6 @@ const ProfileModal = BootstrapModal.extend({
}); });
} }
} }
}); }
_converse.ProfileModal = ProfileModal; api.elements.define('converse-profile-modal', ProfileModal);
export default 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 DOMPurify from 'dompurify';
import { __ } from 'i18n'; import { __ } from 'i18n';
import { api } from "@converse/headless/core"; import { _converse, api } from "@converse/headless/core.js";
import { html } from "lit"; import { html } from "lit";
import { modal_header_close_button } from "plugins/modal/templates/buttons.js"
import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { unsafeHTML } from 'lit/directives/unsafe-html.js';
const tpl_navigation = (o) => { const tpl_navigation = (el) => {
const i18n_about = __('About'); const i18n_about = __('About');
const i18n_commands = __('Commands'); const i18n_commands = __('Commands');
return html` return html`
<ul class="nav nav-pills justify-content-center"> <ul class="nav nav-pills justify-content-center">
<li role="presentation" class="nav-item"> <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>
<li role="presentation" class="nav-item"> <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> </li>
</ul> </ul>
`; `;
} }
export default (o) => { export default (el) => {
const i18n_modal_title = __('Settings');
const first_subtitle = __( const first_subtitle = __(
'%1$s Open Source %2$s XMPP chat client brought to you by %3$s Opkode %2$s', '%1$s Open Source %2$s XMPP chat client brought to you by %3$s Opkode %2$s',
'<a target="_blank" rel="nofollow" href="https://conversejs.org">', '<a 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 show_client_info = api.settings.get('show_client_info');
const allow_adhoc_commands = api.settings.get('allow_adhoc_commands'); const allow_adhoc_commands = api.settings.get('allow_adhoc_commands');
const show_both_tabs = show_client_info && allow_adhoc_commands; const show_both_tabs = show_client_info && allow_adhoc_commands;
return html` return html`
<div class="modal-dialog" role="document"> ${ show_both_tabs ? tpl_navigation(el) : '' }
<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) : '' }
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane tab-pane--columns ${show_client_info ? 'active' : ''}" ${ show_client_info ? html`
<div class="tab-pane tab-pane--columns ${ el.tab === 'about' ? 'active' : ''}"
id="about-tabpanel" role="tabpanel" aria-labelledby="about-tab"> id="about-tabpanel" role="tabpanel" aria-labelledby="about-tab">
<span class="modal-alert"></span> <span class="modal-alert"></span>
<br/> <br/>
<div class="container"> <div class="container">
<h6 class="brand-heading">Converse</h6> <h6 class="brand-heading">Converse</h6>
<p class="brand-subtitle">${o.version_name}</p> <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(first_subtitle))}</p>
<p class="brand-subtitle">${unsafeHTML(DOMPurify.sanitize(second_subtitle))}</p> <p class="brand-subtitle">${unsafeHTML(DOMPurify.sanitize(second_subtitle))}</p>
</div> </div>
</div> </div>` : '' }
<div class="tab-pane tab-pane--columns ${!show_client_info && allow_adhoc_commands ? 'active' : ''}" ${ allow_adhoc_commands ? html`
<div class="tab-pane tab-pane--columns ${ el.tab === 'commands' ? 'active' : ''}"
id="commands-tabpanel" id="commands-tabpanel"
role="tabpanel" role="tabpanel"
aria-labelledby="commands-tab"> aria-labelledby="commands-tab">
<converse-adhoc-commands/> <converse-adhoc-commands/>
</div> </div> ` : '' }
</div>
</div>
</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 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({ constructor (options) {
id: "converse-client-info-modal", super(options);
initialize (settings) { const show_client_info = api.settings.get('show_client_info');
_converse = settings._converse; const allow_adhoc_commands = api.settings.get('allow_adhoc_commands');
BootstrapModal.prototype.initialize.apply(this, arguments); const show_both_tabs = show_client_info && allow_adhoc_commands;
},
toHTML () { if (show_both_tabs || show_client_info) {
return tpl_user_settings_modal( this.tab = 'about';
Object.assign( } else if (allow_adhoc_commands) {
this.model.toJSON(), this.tab = 'commands';
this.model.vcard.toJSON(),
{ 'version_name': _converse.VERSION_NAME }
)
);
} }
}); }
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 tpl_profile from './templates/profile.js';
import { CustomElement } from 'shared/components/element.js'; import { CustomElement } from 'shared/components/element.js';
import { _converse, api } from '@converse/headless/core'; import { _converse, api } from '@converse/headless/core';
class Profile extends CustomElement { class Profile extends CustomElement {
initialize () { initialize () {
this.model = _converse.xmppstatus; this.model = _converse.xmppstatus;
this.listenTo(this.model, "change", () => this.requestUpdate()); this.listenTo(this.model, "change", () => this.requestUpdate());
@ -18,17 +16,17 @@ class Profile extends CustomElement {
showProfileModal (ev) { showProfileModal (ev) {
ev?.preventDefault(); ev?.preventDefault();
api.modal.show(_converse.ProfileModal, {model: this.model}, ev); api.modal.show('converse-profile-modal', { model: this.model }, ev);
} }
showStatusChangeModal (ev) { showStatusChangeModal (ev) {
ev?.preventDefault(); 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(); 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 { html } from "lit";
import { modal_header_close_button } from "plugins/modal/templates/buttons.js"; import { __ } from 'i18n';
export default (o) => html` export default (el) => {
<div class="modal-dialog" role="document"> const label_away = __('Away');
<div class="modal-content"> const label_busy = __('Busy');
<div class="modal-header"> const label_online = __('Online');
<h5 class="modal-title" id="changeStatusModalLabel">${o.modal_title}</h5> const label_save = __('Save');
${modal_header_close_button} const label_xa = __('Away for long');
</div> const placeholder_status_message = __('Personal status message');
<div class="modal-body"> const status = el.model.get('status');
<span class="modal-alert"></span> const status_message = el.model.get('status_message');
<form class="converse-form set-xmpp-status" id="set-xmpp-status">
return html`
<form class="converse-form set-xmpp-status" id="set-xmpp-status" @submit=${ev => el.onFormSubmitted(ev)}>
<div class="form-group"> <div class="form-group">
<div class="custom-control custom-radio"> <div class="custom-control custom-radio">
<input ?checked=${o.status === 'online'} <input ?checked=${status === 'online'}
type="radio" id="radio-online" value="online" name="chat_status" class="custom-control-input"/> type="radio" id="radio-online" value="online" name="chat_status" class="custom-control-input"/>
<label class="custom-control-label" for="radio-online"> <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> <converse-icon size="1em" class="fa fa-circle chat-status chat-status--online"></converse-icon>${label_online}</label>
</div> </div>
<div class="custom-control custom-radio"> <div class="custom-control custom-radio">
<input ?checked=${o.status === 'busy'} <input ?checked=${status === 'busy'}
type="radio" id="radio-busy" value="dnd" name="chat_status" class="custom-control-input"/> type="radio" id="radio-busy" value="dnd" name="chat_status" class="custom-control-input"/>
<label class="custom-control-label" for="radio-busy"> <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> <converse-icon size="1em" class="fa fa-minus-circle chat-status chat-status--busy"></converse-icon>${label_busy}</label>
</div> </div>
<div class="custom-control custom-radio"> <div class="custom-control custom-radio">
<input ?checked=${o.status === 'away'} <input ?checked=${status === 'away'}
type="radio" id="radio-away" value="away" name="chat_status" class="custom-control-input"/> type="radio" id="radio-away" value="away" name="chat_status" class="custom-control-input"/>
<label class="custom-control-label" for="radio-away"> <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> <converse-icon size="1em" class="fa fa-circle chat-status chat-status--away"></converse-icon>${label_away}</label>
</div> </div>
<div class="custom-control custom-radio"> <div class="custom-control custom-radio">
<input ?checked=${o.status === 'xa'} <input ?checked=${status === 'xa'}
type="radio" id="radio-xa" value="xa" name="chat_status" class="custom-control-input"/> type="radio" id="radio-xa" value="xa" name="chat_status" class="custom-control-input"/>
<label class="custom-control-label" for="radio-xa"> <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> <converse-icon size="1em" class="far fa-circle chat-status chat-status--xa"></converse-icon>${label_xa}</label>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="btn-group w-100"> <div class="btn-group w-100">
<input name="status_message" type="text" class="form-control" <input name="status_message" type="text" class="form-control" autofocus
value="${o.status_message || ''}" placeholder="${o.placeholder_status_message}"/> value="${status_message || ''}" placeholder="${placeholder_status_message}"/>
<converse-icon size="1em" class="fa fa-times clear-input ${o.status_message ? '' : 'hidden'}"></converse-icon> <converse-icon size="1em" class="fa fa-times clear-input ${status_message ? '' : 'hidden'}" @click=${ev => el.clearStatusMessage(ev)}></converse-icon>
</div> </div>
</div> </div>
<button type="submit" class="btn btn-primary">${o.label_save}</button> <button type="submit" class="btn btn-primary">${label_save}</button>
</form> </form>`;
</div> }
</div>
</div>
`;

View File

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

View File

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

View File

@ -255,9 +255,9 @@ describe("A groupchat shown in the groupchats list", function () {
const info_el = rooms_list.querySelector(".room-info"); const info_el = rooms_list.querySelector(".room-info");
info_el.click(); info_el.click();
const modal = _converse.api.modal.get('muc-details-modal'); const modal = _converse.api.modal.get('converse-muc-details-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000); await u.waitUntil(() => u.isVisible(modal), 1000);
let els = modal.el.querySelectorAll('p.room-info'); let els = modal.querySelectorAll('p.room-info');
expect(els[0].textContent).toBe("Name: A Dark Cave") expect(els[0].textContent).toBe("Name: A Dark Cave")
expect(els[1].querySelector('strong').textContent).toBe("XMPP address"); 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[2].querySelector('converse-rich-text').textContent).toBe("This is the description");
expect(els[3].textContent).toBe("Online users: 1") 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( expect(features_list.textContent.replace(/(\n|\s{2,})/g, '')).toBe(
'Password protected - This groupchat requires a password before entry'+ 'Password protected - This groupchat requires a password before entry'+
'Hidden - This groupchat is not publicly searchable'+ '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)); _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") expect(els[3].textContent).toBe("Online users: 2")
view.model.set({'subject': {'author': 'someone', 'text': 'Hatching dark plots'}}); 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[0].textContent).toBe("Name: A Dark Cave")
expect(els[1].querySelector('strong').textContent).toBe("XMPP address"); 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 RoomsListModel from './model.js';
import tpl_roomslist from "./templates/roomslist.js"; import tpl_roomslist from "./templates/roomslist.js";
import { CustomElement } from 'shared/components/element.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 jid = ev.currentTarget.getAttribute('data-room-jid');
const room = _converse.chatboxes.get(jid); const room = _converse.chatboxes.get(jid);
ev.preventDefault(); 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 async openRoom (ev) { // eslint-disable-line class-methods-use-this

View File

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

View File

@ -2,7 +2,6 @@ import { __ } from 'i18n';
import { api } from '@converse/headless/core.js'; import { api } from '@converse/headless/core.js';
import { getGroupsAutoCompleteList } from '@converse/headless/plugins/roster/utils.js'; import { getGroupsAutoCompleteList } from '@converse/headless/plugins/roster/utils.js';
import { html } from "lit"; import { html } from "lit";
import { modal_header_close_button } from "plugins/modal/templates/buttons.js"
export default (el) => { export default (el) => {
@ -10,16 +9,10 @@ export default (el) => {
const i18n_contact_placeholder = __('name@example.org'); const i18n_contact_placeholder = __('name@example.org');
const i18n_error_message = __('Please enter a valid XMPP address'); const i18n_error_message = __('Please enter a valid XMPP address');
const i18n_group = __('Group'); const i18n_group = __('Group');
const i18n_new_contact = __('Add a Contact');
const i18n_nickname = __('Name'); const i18n_nickname = __('Name');
const i18n_xmpp_address = __('XMPP Address'); const i18n_xmpp_address = __('XMPP Address');
return html` 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)}> <form class="converse-form add-xmpp-contact" @submit=${ev => el.addContactFromForm(ev)}>
<div class="modal-body"> <div class="modal-body">
<span class="modal-alert"></span> <span class="modal-alert"></span>
@ -44,17 +37,12 @@ export default (el) => {
<span class="suggestion-box__additions visually-hidden" role="status" aria-live="assertive" aria-relevant="additions"></span> <span class="suggestion-box__additions visually-hidden" role="status" aria-live="assertive" aria-relevant="additions"></span>
</div> </div>
</div> </div>
<div class="form-group add-xmpp-contact__group"> <div class="form-group add-xmpp-contact__group">
<label class="clearfix" for="name">${i18n_group}:</label> <label class="clearfix" for="name">${i18n_group}:</label>
<converse-autocomplete .list=${getGroupsAutoCompleteList()} name="group"></converse-autocomplete> <converse-autocomplete .list=${getGroupsAutoCompleteList()} name="group"></converse-autocomplete>
</div> </div>
<div class="form-group"><div class="invalid-feedback">${i18n_error_message}</div></div> <div class="form-group"><div class="invalid-feedback">${i18n_error_message}</div></div>
<button type="submit" class="btn btn-primary">${i18n_add}</button> <button type="submit" class="btn btn-primary">${i18n_add}</button>
</div> </div>
</form> </form>`;
</div>
</div>
`;
} }

View File

@ -13,13 +13,14 @@ export default class RosterView extends CustomElement {
async initialize () { async initialize () {
await api.waitUntil('rosterInitialized') await api.waitUntil('rosterInitialized')
const { presences, roster } = _converse;
this.listenTo(_converse, 'rosterContactsFetched', () => this.requestUpdate()); this.listenTo(_converse, 'rosterContactsFetched', () => this.requestUpdate());
this.listenTo(_converse.presences, 'change:show', () => this.requestUpdate()); this.listenTo(presences, 'change:show', () => this.requestUpdate());
this.listenTo(_converse.roster, 'add', () => this.requestUpdate()); this.listenTo(roster, 'add', () => this.requestUpdate());
this.listenTo(_converse.roster, 'destroy', () => this.requestUpdate()); this.listenTo(roster, 'destroy', () => this.requestUpdate());
this.listenTo(_converse.roster, 'remove', () => this.requestUpdate()); this.listenTo(roster, 'remove', () => this.requestUpdate());
this.listenTo(_converse.roster, 'change', () => this.requestUpdate()); this.listenTo(roster, 'change', () => this.requestUpdate());
this.listenTo(_converse.roster.state, 'change', () => this.requestUpdate()); this.listenTo(roster.state, 'change', () => this.requestUpdate());
/** /**
* Triggered once the _converse.RosterView instance has been created and initialized. * Triggered once the _converse.RosterView instance has been created and initialized.
* @event _converse#rosterViewInitialized * @event _converse#rosterViewInitialized
@ -42,7 +43,7 @@ export default class RosterView extends CustomElement {
} }
showAddContactModal (ev) { // eslint-disable-line class-methods-use-this 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 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 cbview = _converse.chatboxviews.get('controlbox');
const change_status_el = await u.waitUntil(() => cbview.querySelector('.change-status')); const change_status_el = await u.waitUntil(() => cbview.querySelector('.change-status'));
change_status_el.click() change_status_el.click()
let modal = _converse.api.modal.get('modal-status-change'); let 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 = 'My custom status'; const msg = 'My custom status';
modal.el.querySelector('input[name="status_message"]').value = msg; modal.querySelector('input[name="status_message"]').value = msg;
modal.el.querySelector('[type="submit"]').click(); modal.querySelector('[type="submit"]').click();
const sent_stanzas = _converse.connection.sent_stanzas; const sent_stanzas = _converse.connection.sent_stanzas;
let sent_presence = await u.waitUntil(() => sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).pop()); 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>`+ `<priority>0</priority>`+
`<c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>`+ `<c hash="sha-1" node="https://conversejs.org" ver="TfHz9vOOfqIG0Z9lW5CuPaWGnrQ=" xmlns="http://jabber.org/protocol/caps"/>`+
`</presence>`) `</presence>`)
await u.waitUntil(() => modal.el.getAttribute('aria-hidden') === "true"); await u.waitUntil(() => modal.getAttribute('aria-hidden') === "true");
await u.waitUntil(() => !u.isVisible(modal.el)); await u.waitUntil(() => !u.isVisible(modal));
cbview.querySelector('.change-status').click() cbview.querySelector('.change-status').click()
modal = _converse.api.modal.get('modal-status-change'); modal = _converse.api.modal.get('converse-chat-status-modal');
await u.waitUntil(() => modal.el.getAttribute('aria-hidden') === "false", 1000); await u.waitUntil(() => modal.getAttribute('aria-hidden') === "false", 1000);
modal.el.querySelector('label[for="radio-busy"]').click(); // Change status to "dnd" modal.querySelector('label[for="radio-busy"]').click(); // Change status to "dnd"
modal.el.querySelector('[type="submit"]').click(); modal.querySelector('[type="submit"]').click();
await u.waitUntil(() => sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).length === 2); 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(); sent_presence = sent_stanzas.filter(s => Strophe.serialize(s).match('presence')).pop();
expect(Strophe.serialize(sent_presence)) expect(Strophe.serialize(sent_presence))

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import 'shared/registry.js'; 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 renderRichText from 'shared/directives/rich-text.js';
import { CustomElement } from 'shared/components/element.js'; import { CustomElement } from 'shared/components/element.js';
import { api } from "@converse/headless/core"; 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 onImgClick (ev) { // eslint-disable-line class-methods-use-this
ev.preventDefault(); 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 () { onImgLoad () {

View File

@ -1,11 +1,11 @@
import './message-actions.js'; import './message-actions.js';
import './message-body.js'; import './message-body.js';
import 'shared/components/dropdown.js'; import 'shared/components/dropdown.js';
import 'shared/modals/message-versions.js';
import 'shared/modals/user-details.js';
import 'shared/registry'; import 'shared/registry';
import 'plugins/muc-views/modals/occupant.js';
import tpl_file_progress from './templates/file-progress.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 log from '@converse/headless/log';
import tpl_info_message from './templates/info-message.js'; import tpl_info_message from './templates/info-message.js';
import tpl_mep_message from 'plugins/muc-views/templates/mep-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) { showUserModal (ev) {
if (this.model.get('sender') === 'me') { 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') { } else if (this.model.get('type') === 'groupchat') {
ev.preventDefault(); 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 { } else {
ev.preventDefault(); ev.preventDefault();
const chatbox = this.model.collection.chatbox; 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) { showMessageVersionsModal (ev) {
ev.preventDefault(); ev.preventDefault();
api.modal.show(MessageVersionsModal, {'model': this.model}, ev); api.modal.show('converse-message-versions-modal', {'model': this.model}, ev);
} }
toggleSpoilerMessage (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); const view = _converse.chatboxviews.get(contact_jid);
let show_modal_button = view.querySelector('.show-user-details-modal'); let show_modal_button = view.querySelector('.show-user-details-modal');
show_modal_button.click(); show_modal_button.click();
const modal = _converse.api.modal.get('user-details-modal'); const modal = _converse.api.modal.get('converse-user-details-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000); await u.waitUntil(() => u.isVisible(modal), 1000);
spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true)); spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true));
spyOn(view.model.contact, 'removeFromRoster').and.callFake(callback => callback()); 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(); expect(u.isVisible(remove_contact_button)).toBeTruthy();
remove_contact_button.click(); remove_contact_button.click();
await u.waitUntil(() => modal.el.getAttribute('aria-hidden'), 1000); await u.waitUntil(() => modal.getAttribute('aria-hidden'), 1000);
await u.waitUntil(() => !u.isVisible(modal.el)); await u.waitUntil(() => !u.isVisible(modal));
show_modal_button = view.querySelector('.show-user-details-modal'); show_modal_button = view.querySelector('.show-user-details-modal');
show_modal_button.click(); 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(); expect(remove_contact_button === null).toBeTruthy();
})); }));
@ -44,15 +44,15 @@ describe("The User Details Modal", function () {
const view = _converse.chatboxviews.get(contact_jid); const view = _converse.chatboxviews.get(contact_jid);
let show_modal_button = view.querySelector('.show-user-details-modal'); let show_modal_button = view.querySelector('.show-user-details-modal');
show_modal_button.click(); show_modal_button.click();
let modal = _converse.api.modal.get('user-details-modal'); let modal = _converse.api.modal.get('converse-user-details-modal');
await u.waitUntil(() => u.isVisible(modal.el), 2000); await u.waitUntil(() => u.isVisible(modal), 2000);
spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true)); spyOn(_converse.api, 'confirm').and.returnValue(Promise.resolve(true));
spyOn(view.model.contact, 'removeFromRoster').and.callFake((callback, errback) => errback()); 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(); expect(u.isVisible(remove_contact_button)).toBeTruthy();
remove_contact_button.click(); 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); await u.waitUntil(() => u.isVisible(document.querySelector('.alert-danger')), 2000);
const header = document.querySelector('.alert-danger .modal-title'); 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(); document.querySelector('.alert-danger button.close').click();
show_modal_button = view.querySelector('.show-user-details-modal'); show_modal_button = view.querySelector('.show-user-details-modal');
show_modal_button.click(); show_modal_button.click();
modal = _converse.api.modal.get('user-details-modal'); modal = _converse.api.modal.get('converse-user-details-modal');
await u.waitUntil(() => u.isVisible(modal.el), 2000) await u.waitUntil(() => u.isVisible(modal), 2000)
show_modal_button = view.querySelector('.show-user-details-modal'); show_modal_button = view.querySelector('.show-user-details-modal');
show_modal_button.click(); 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(); 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 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 { __ } 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; const u = converse.env.utils;
function removeContact (contact) { export default class UserDetailsModal extends BaseModal {
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',
},
initialize () { initialize () {
BootstrapModal.prototype.initialize.apply(this, arguments); super.initialize();
this.model.rosterContactAdded.then(() => this.registerContactEventHandlers()); this.model.rosterContactAdded.then(() => this.registerContactEventHandlers());
this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'change', this.render);
this.registerContactEventHandlers(); this.registerContactEventHandlers();
@ -41,23 +22,19 @@ const UserDetailsModal = BootstrapModal.extend({
* @example _converse.api.listen.on('userDetailsModalInitialized', (chatbox) => { ... }); * @example _converse.api.listen.on('userDetailsModalInitialized', (chatbox) => { ... });
*/ */
api.trigger('userDetailsModalInitialized', this.model); api.trigger('userDetailsModalInitialized', this.model);
}, }
toHTML () { renderModal () {
const vcard = this.model?.vcard; return tpl_user_details_modal(this);
const vcard_json = vcard ? vcard.toJSON() : {}; }
return tpl_user_details_modal(Object.assign(
this.model.toJSON(), renderModalFooter () {
vcard_json, { return tpl_footer(this);
'_converse': _converse, }
'allow_contact_removal': api.settings.get('allow_contact_removal'),
'display_name': this.model.getDisplayName(), getModalTitle () {
'is_roster_contact': this.model.contact !== undefined, return this.model.getDisplayName();
'removeContact': ev => this.removeContact(ev), }
'view': this,
'utils': u
}));
},
registerContactEventHandlers () { registerContactEventHandlers () {
if (this.model.contact !== undefined) { if (this.model.contact !== undefined) {
@ -68,7 +45,7 @@ const UserDetailsModal = BootstrapModal.extend({
this.render(); this.render();
}); });
} }
}, }
async refreshContact (ev) { async refreshContact (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); } 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'); this.alert(__('Sorry, something went wrong while trying to refresh'), 'danger');
} }
u.removeClass('fa-spin', refresh_icon); u.removeClass('fa-spin', refresh_icon);
}, }
async removeContact (ev) { async removeContact (ev) {
ev?.preventDefault?.(); ev?.preventDefault?.();
@ -94,9 +71,7 @@ const UserDetailsModal = BootstrapModal.extend({
setTimeout(() => removeContact(this.model.contact), 1); setTimeout(() => removeContact(this.model.contact), 1);
this.modal.hide(); this.modal.hide();
} }
}, }
}); }
_converse.UserDetailsModal = UserDetailsModal; api.elements.define('converse-user-details-modal', UserDetailsModal);
export default UserDetailsModal;

View File

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

View File

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

View File

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