Render the MUC view component declaratively

This commit is contained in:
JC Brand 2021-03-13 11:37:07 +01:00
parent 851bfc61e0
commit d8daedea0d
26 changed files with 446 additions and 508 deletions

View File

@ -1,3 +1,15 @@
converse-muc-config-form {
width: 100%;
overflow: auto;
}
converse-muc-disconnected,
converse-muc-destroyed {
padding: 2em;
width: 100%;
height: 100%;
}
#conversejs.converse-embedded, #conversejs.converse-embedded,
#conversejs { #conversejs {
.badge--muc { .badge--muc {
@ -324,7 +336,7 @@
} }
} }
} }
.chatroom-form-container { .muc-form-container {
background-color: white; background-color: white;
border: 0; border: 0;
color: var(--text-color); color: var(--text-color);
@ -376,7 +388,7 @@
padding: 0; padding: 0;
height: 16em; height: 16em;
.chatroom-form-container { .muc-form-container {
.chatroom-form { .chatroom-form {
padding-top: 2em; padding-top: 2em;
padding-bottom: 0; padding-bottom: 0;
@ -528,7 +540,7 @@
} }
.chatroom-body { .chatroom-body {
height: 100%; height: 100%;
.chatroom-form-container { .muc-form-container {
height: 100%; height: 100%;
position: relative; position: relative;
} }

View File

@ -118,6 +118,10 @@
padding-bottom: 0; padding-bottom: 0;
} }
&.converse-form--spinner {
height: 100%;
}
&.converse-centered-form { &.converse-centered-form {
min-height: 66%; min-height: 66%;
text-align: center; text-align: center;

View File

@ -27,21 +27,20 @@ describe("A chat room", function () {
await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED)); await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED));
await mock.returnMemberLists(_converse, muc_jid, [], ['member', 'admin', 'owner']); await mock.returnMemberLists(_converse, muc_jid, [], ['member', 'admin', 'owner']);
spyOn(view, 'renderBookmarkForm').and.callThrough();
spyOn(view, 'closeForm').and.callThrough();
await u.waitUntil(() => view.querySelector('.toggle-bookmark') !== null); await u.waitUntil(() => view.querySelector('.toggle-bookmark') !== null);
const toggle = view.querySelector('.toggle-bookmark'); const toggle = view.querySelector('.toggle-bookmark');
expect(toggle.title).toBe('Bookmark this groupchat'); expect(toggle.title).toBe('Bookmark this groupchat');
toggle.click(); toggle.click();
expect(view.renderBookmarkForm).toHaveBeenCalled();
view.querySelector('.button-cancel').click(); const cancel_button = await u.waitUntil(() => view.querySelector('.button-cancel'));
expect(view.closeForm).toHaveBeenCalled(); expect(view.model.session.get('view')).toBe('bookmark-form');
cancel_button.click();
await u.waitUntil(() => view.model.session.get('view') === null);
expect(u.hasClass('on-button', toggle), false); expect(u.hasClass('on-button', toggle), false);
expect(toggle.title).toBe('Bookmark this groupchat'); expect(toggle.title).toBe('Bookmark this groupchat');
toggle.click(); toggle.click();
expect(view.renderBookmarkForm).toHaveBeenCalled();
/* Client uploads data: /* Client uploads data:
* -------------------- * --------------------
@ -75,13 +74,13 @@ describe("A chat room", function () {
* </iq> * </iq>
*/ */
expect(view.model.get('bookmarked')).toBeFalsy(); expect(view.model.get('bookmarked')).toBeFalsy();
const form = view.querySelector('.chatroom-form'); const form = await u.waitUntil(() => view.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;
view.querySelector('.muc-bookmark-form .btn-primary').click(); view.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());

View File

@ -314,19 +314,19 @@ describe("A sent groupchat message", function () {
'stopPropagation': function stopPropagation () {}, 'stopPropagation': function stopPropagation () {},
'keyCode': 13 // Enter 'keyCode': 13 // Enter
} }
spyOn(_converse.connection, 'send');
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter_event); bottom_panel.onKeyDown(enter_event);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
const msg = _converse.connection.send.calls.all()[0].args[0]; const sent_stanzas = _converse.connection.sent_stanzas;
expect(msg.toLocaleString()) const msg = await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName.toLowerCase() === 'message').pop());
.toBe(`<message from="romeo@montague.lit/orchard" id="${msg.nodeTree.getAttribute("id")}" `+ expect(Strophe.serialize(msg))
.toBe(`<message from="romeo@montague.lit/orchard" id="${msg.getAttribute("id")}" `+
`to="lounge@montague.lit" type="groupchat" `+ `to="lounge@montague.lit" type="groupchat" `+
`xmlns="jabber:client">`+ `xmlns="jabber:client">`+
`<body>hello Link Mauve</body>`+ `<body>hello Link Mauve</body>`+
`<active xmlns="http://jabber.org/protocol/chatstates"/>`+ `<active xmlns="http://jabber.org/protocol/chatstates"/>`+
`<reference begin="6" end="16" type="mention" uri="xmpp:lounge@montague.lit/Link%20Mauve" xmlns="urn:xmpp:reference:0"/>`+ `<reference begin="6" end="16" type="mention" uri="xmpp:lounge@montague.lit/Link%20Mauve" xmlns="urn:xmpp:reference:0"/>`+
`<origin-id id="${msg.nodeTree.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+ `<origin-id id="${msg.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+
`</message>`); `</message>`);
done(); done();
})); }));
@ -374,7 +374,6 @@ describe("A sent groupchat message", function () {
'stopPropagation': function stopPropagation () {}, 'stopPropagation': function stopPropagation () {},
'keyCode': 13 // Enter 'keyCode': 13 // Enter
} }
spyOn(_converse.connection, 'send');
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter_event); bottom_panel.onKeyDown(enter_event);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
@ -385,9 +384,10 @@ describe("A sent groupchat message", function () {
'hello <span class="mention">z3r0</span> <span class="mention">gibson</span> <span class="mention">mr.robot</span>, how are you?' 'hello <span class="mention">z3r0</span> <span class="mention">gibson</span> <span class="mention">mr.robot</span>, how are you?'
); );
const msg = _converse.connection.send.calls.all()[0].args[0]; const sent_stanzas = _converse.connection.sent_stanzas;
expect(msg.toLocaleString()) const msg = await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName.toLowerCase() === 'message').pop());
.toBe(`<message from="romeo@montague.lit/orchard" id="${msg.nodeTree.getAttribute("id")}" `+ expect(Strophe.serialize(msg))
.toBe(`<message from="romeo@montague.lit/orchard" id="${msg.getAttribute("id")}" `+
`to="lounge@montague.lit" type="groupchat" `+ `to="lounge@montague.lit" type="groupchat" `+
`xmlns="jabber:client">`+ `xmlns="jabber:client">`+
`<body>hello z3r0 gibson mr.robot, how are you?</body>`+ `<body>hello z3r0 gibson mr.robot, how are you?</body>`+
@ -395,7 +395,7 @@ describe("A sent groupchat message", function () {
`<reference begin="6" end="10" type="mention" uri="xmpp:z3r0@montague.lit" xmlns="urn:xmpp:reference:0"/>`+ `<reference begin="6" end="10" type="mention" uri="xmpp:z3r0@montague.lit" xmlns="urn:xmpp:reference:0"/>`+
`<reference begin="11" end="17" type="mention" uri="xmpp:gibson@montague.lit" xmlns="urn:xmpp:reference:0"/>`+ `<reference begin="11" end="17" type="mention" uri="xmpp:gibson@montague.lit" xmlns="urn:xmpp:reference:0"/>`+
`<reference begin="18" end="26" type="mention" uri="xmpp:mr.robot@montague.lit" xmlns="urn:xmpp:reference:0"/>`+ `<reference begin="18" end="26" type="mention" uri="xmpp:mr.robot@montague.lit" xmlns="urn:xmpp:reference:0"/>`+
`<origin-id id="${msg.nodeTree.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+ `<origin-id id="${msg.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+
`</message>`); `</message>`);
const action = await u.waitUntil(() => view.querySelector('.chat-msg .chat-msg__action')); const action = await u.waitUntil(() => view.querySelector('.chat-msg .chat-msg__action'));
@ -406,16 +406,15 @@ describe("A sent groupchat message", function () {
expect(view.model.messages.at(0).get('correcting')).toBe(true); expect(view.model.messages.at(0).get('correcting')).toBe(true);
expect(view.querySelectorAll('.chat-msg').length).toBe(1); expect(view.querySelectorAll('.chat-msg').length).toBe(1);
await u.waitUntil(() => u.hasClass('correcting', view.querySelector('.chat-msg')), 500); await u.waitUntil(() => u.hasClass('correcting', view.querySelector('.chat-msg')), 500);
await u.waitUntil(() => _converse.connection.send.calls.count() === 1);
textarea.value = 'hello @z3r0 @gibson @sw0rdf1sh, how are you?'; textarea.value = 'hello @z3r0 @gibson @sw0rdf1sh, how are you?';
bottom_panel.onKeyDown(enter_event); bottom_panel.onKeyDown(enter_event);
await u.waitUntil(() => view.querySelector('.chat-msg__text').textContent === await u.waitUntil(() => view.querySelector('.chat-msg__text').textContent ===
'hello z3r0 gibson sw0rdf1sh, how are you?', 500); 'hello z3r0 gibson sw0rdf1sh, how are you?', 500);
const correction = _converse.connection.send.calls.all()[1].args[0]; const correction = sent_stanzas.filter(s => s.nodeName.toLowerCase() === 'message').pop();
expect(correction.toLocaleString()) expect(Strophe.serialize(correction))
.toBe(`<message from="romeo@montague.lit/orchard" id="${correction.nodeTree.getAttribute("id")}" `+ .toBe(`<message from="romeo@montague.lit/orchard" id="${correction.getAttribute("id")}" `+
`to="lounge@montague.lit" type="groupchat" `+ `to="lounge@montague.lit" type="groupchat" `+
`xmlns="jabber:client">`+ `xmlns="jabber:client">`+
`<body>hello z3r0 gibson sw0rdf1sh, how are you?</body>`+ `<body>hello z3r0 gibson sw0rdf1sh, how are you?</body>`+
@ -423,8 +422,8 @@ describe("A sent groupchat message", function () {
`<reference begin="6" end="10" type="mention" uri="xmpp:z3r0@montague.lit" xmlns="urn:xmpp:reference:0"/>`+ `<reference begin="6" end="10" type="mention" uri="xmpp:z3r0@montague.lit" xmlns="urn:xmpp:reference:0"/>`+
`<reference begin="11" end="17" type="mention" uri="xmpp:gibson@montague.lit" xmlns="urn:xmpp:reference:0"/>`+ `<reference begin="11" end="17" type="mention" uri="xmpp:gibson@montague.lit" xmlns="urn:xmpp:reference:0"/>`+
`<reference begin="18" end="27" type="mention" uri="xmpp:sw0rdf1sh@montague.lit" xmlns="urn:xmpp:reference:0"/>`+ `<reference begin="18" end="27" type="mention" uri="xmpp:sw0rdf1sh@montague.lit" xmlns="urn:xmpp:reference:0"/>`+
`<replace id="${msg.nodeTree.getAttribute("id")}" xmlns="urn:xmpp:message-correct:0"/>`+ `<replace id="${msg.getAttribute("id")}" xmlns="urn:xmpp:message-correct:0"/>`+
`<origin-id id="${correction.nodeTree.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+ `<origin-id id="${correction.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+
`</message>`); `</message>`);
done(); done();
})); }));

View File

@ -1281,6 +1281,9 @@ describe("A Chat Message", function () {
await mock.openChatBoxFor(_converse, contact_jid); await mock.openChatBoxFor(_converse, contact_jid);
const messages = _converse.connection.sent_stanzas.filter(s => s.nodeName === 'message'); const messages = _converse.connection.sent_stanzas.filter(s => s.nodeName === 'message');
if (messages.length > 1) {
debugger;
}
expect(messages.length).toBe(1); expect(messages.length).toBe(1);
expect(Strophe.serialize(messages[0])).toBe( expect(Strophe.serialize(messages[0])).toBe(
`<message id="${messages[0].getAttribute('id')}" to="tybalt@montague.lit" type="chat" xmlns="jabber:client">`+ `<message id="${messages[0].getAttribute('id')}" to="tybalt@montague.lit" type="chat" xmlns="jabber:client">`+

View File

@ -1,7 +1,7 @@
/*global mock, converse */ /*global mock, converse */
const Model = converse.env.Model; const Model = converse.env.Model;
const { sizzle, u } = converse.env; const { $pres, $iq, Strophe, sizzle, u } = converse.env;
describe("Groupchats", function () { describe("Groupchats", function () {

View File

@ -147,7 +147,6 @@ describe("Groupchats", function () {
return done(); return done();
})); }));
it("maintains its state across reloads", it("maintains its state across reloads",
mock.initConverse([], { mock.initConverse([], {
'clear_messages_on_reconnection': true, 'clear_messages_on_reconnection': true,
@ -248,7 +247,6 @@ describe("Groupchats", function () {
`<set xmlns="http://jabber.org/protocol/rsm"><before></before><max>50</max></set>`+ `<set xmlns="http://jabber.org/protocol/rsm"><before></before><max>50</max></set>`+
`</query>`+ `</query>`+
`</iq>`); `</iq>`);
done(); done();
})); }));
@ -567,7 +565,7 @@ describe("Groupchats", function () {
const muc_jid = 'lounge@montague.lit'; const muc_jid = 'lounge@montague.lit';
await mock.openAndEnterChatRoom(_converse, muc_jid , 'romeo'); await mock.openAndEnterChatRoom(_converse, muc_jid , 'romeo');
const view = _converse.chatboxviews.get(muc_jid); const model = _converse.chatboxes.get(muc_jid);
const message = 'Hello world', const message = 'Hello world',
nick = mock.chatroom_names[0], nick = mock.chatroom_names[0],
msg = $msg({ msg = $msg({
@ -577,18 +575,19 @@ describe("Groupchats", function () {
'type': 'groupchat' 'type': 'groupchat'
}).c('body').t(message).tree(); }).c('body').t(message).tree();
await view.model.handleMessageStanza(msg); await model.handleMessageStanza(msg);
await view.model.close(); await u.waitUntil(() => document.querySelector('converse-chat-message'));
await model.close();
await u.waitUntil(() => !document.querySelector('converse-chat-message'));
_converse.connection.IQ_stanzas = []; _converse.connection.IQ_stanzas = [];
await mock.openAndEnterChatRoom(_converse, muc_jid , 'romeo'); await mock.openAndEnterChatRoom(_converse, muc_jid , 'romeo');
await u.waitUntil(() => view.querySelector('converse-chat-message')); await u.waitUntil(() => document.querySelector('converse-chat-message'));
expect(view.model.messages.length).toBe(1); expect(model.messages.length).toBe(1);
expect(view.querySelectorAll('converse-chat-message').length).toBe(1); expect(document.querySelectorAll('converse-chat-message').length).toBe(1);
done() done()
})); }));
it("clears cached messages when it reconnects and clear_messages_on_reconnection is true", it("clears cached messages when it reconnects and clear_messages_on_reconnection is true",
mock.initConverse([], {'clear_messages_on_reconnection': true}, async function (done, _converse) { mock.initConverse([], {'clear_messages_on_reconnection': true}, async function (done, _converse) {
@ -734,7 +733,7 @@ describe("Groupchats", function () {
.c('status', {code: '110'}); .c('status', {code: '110'});
_converse.connection._dataRecv(mock.createRequest(presence)); _converse.connection._dataRecv(mock.createRequest(presence));
const csntext = await u.waitUntil(() => view.querySelector('.chat-content__notifications').textContent); const csntext = await u.waitUntil(() => view.querySelector('.chat-content__notifications')?.textContent);
expect(csntext.trim()).toEqual("some1 has entered the groupchat"); expect(csntext.trim()).toEqual("some1 has entered the groupchat");
await room_creation_promise; await room_creation_promise;
@ -1228,6 +1227,10 @@ describe("Groupchats", function () {
.c('status', {code: '110'}); .c('status', {code: '110'});
_converse.connection._dataRecv(mock.createRequest(presence)); _converse.connection._dataRecv(mock.createRequest(presence));
await u.waitUntil(() => view.querySelector('.configure-chatroom-button') !== null); await u.waitUntil(() => view.querySelector('.configure-chatroom-button') !== null);
const own_occupant = view.model.getOwnOccupant();
await u.waitUntil(() => own_occupant.get('affiliation') === 'owner');
view.querySelector('.configure-chatroom-button').click(); view.querySelector('.configure-chatroom-button').click();
/* Check that an IQ is sent out, asking for the /* Check that an IQ is sent out, asking for the
@ -1470,7 +1473,8 @@ describe("Groupchats", function () {
view.model.rejoin(); view.model.rejoin();
// Test that members aren't removed when we reconnect // Test that members aren't removed when we reconnect
expect(view.model.occupants.length).toBe(8); expect(view.model.occupants.length).toBe(8);
await u.waitUntil(() => occupants.querySelectorAll('li').length === 8); view.model.session.set('connection_status', converse.ROOMSTATUS.ENTERED); // Hack
await u.waitUntil(() => view.querySelectorAll('.occupant-list li').length === 8);
done(); done();
})); }));
@ -1607,12 +1611,12 @@ describe("Groupchats", function () {
const view = _converse.chatboxviews.get('problematic@muc.montague.lit'); const view = _converse.chatboxviews.get('problematic@muc.montague.lit');
_converse.connection._dataRecv(mock.createRequest(presence)); _converse.connection._dataRecv(mock.createRequest(presence));
expect(view.querySelector('.chatroom-body .disconnect-msg').textContent.trim()) const msg = await u.waitUntil(() => view.querySelector('.chatroom-body .disconnect-msg'));
.toBe('This groupchat no longer exists'); expect(msg.textContent.trim()).toBe('This groupchat no longer exists');
expect(view.querySelector('.chatroom-body .destroyed-reason').textContent.trim()) expect(view.querySelector('.chatroom-body .destroyed-reason').textContent.trim())
.toBe(`"We didn't like the name"`); .toBe(`The following reason was given: "We didn't like the name"`);
expect(view.querySelector('.chatroom-body .moved-label').textContent.trim()) expect(view.querySelector('.chatroom-body .moved-label').textContent.trim())
.toBe('The conversation has moved. Click below to enter.'); .toBe('The conversation has moved to a new address. Click the link below to enter.');
expect(view.querySelector('.chatroom-body .moved-link').textContent.trim()) expect(view.querySelector('.chatroom-body .moved-link').textContent.trim())
.toBe(`other-room@chat.jabberfr.org`); .toBe(`other-room@chat.jabberfr.org`);
done(); done();
@ -1649,11 +1653,10 @@ describe("Groupchats", function () {
* node='x-roomuser-item'/> * node='x-roomuser-item'/>
* </iq> * </iq>
*/ */
const iq = await u.waitUntil(() => _.filter( const iq = await u.waitUntil(() => IQ_stanzas.filter(
IQ_stanzas,
s => sizzle(`iq[to="${muc_jid}"] query[node="x-roomuser-item"]`, s).length s => sizzle(`iq[to="${muc_jid}"] query[node="x-roomuser-item"]`, s).length
).pop() ).pop());
);
expect(Strophe.serialize(iq)).toBe( expect(Strophe.serialize(iq)).toBe(
`<iq from="romeo@montague.lit/orchard" id="${iq.getAttribute('id')}" to="lounge@montague.lit" `+ `<iq from="romeo@montague.lit/orchard" id="${iq.getAttribute('id')}" to="lounge@montague.lit" `+
`type="get" xmlns="jabber:client">`+ `type="get" xmlns="jabber:client">`+
@ -2116,8 +2119,7 @@ describe("Groupchats", function () {
const muc_jid = 'coven@chat.shakespeare.lit'; const muc_jid = 'coven@chat.shakespeare.lit';
await _converse.api.rooms.open(muc_jid, { nick }); await _converse.api.rooms.open(muc_jid, { nick });
const stanza = await u.waitUntil(() => _.filter( const stanza = await u.waitUntil(() => IQ_stanzas.filter(
IQ_stanzas,
iq => iq.querySelector( iq => iq.querySelector(
`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
)).pop()); )).pop());
@ -2195,13 +2197,13 @@ describe("Groupchats", function () {
'muc_unmoderated', 'muc_unmoderated',
'muc_nonanonymous' 'muc_nonanonymous'
]; ];
await mock.openAndEnterChatRoom(_converse, 'room@conference.example.org', 'romeo', features); const muc_jid = 'room@conference.example.org';
const jid = 'room@conference.example.org'; await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
const view = _converse.chatboxviews.get(jid); const view = _converse.chatboxviews.get(muc_jid);
const info_el = view.querySelector(".show-muc-details-modal"); const info_el = view.querySelector(".show-muc-details-modal");
info_el.click(); info_el.click();
const modal = _converse.api.modal.get('muc-details-modal'); let modal = _converse.api.modal.get('muc-details-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000); await u.waitUntil(() => u.isVisible(modal.el), 1000);
let features_list = modal.el.querySelector('.features-list'); let features_list = modal.el.querySelector('.features-list');
@ -2228,14 +2230,12 @@ 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();
view.querySelector('.configure-chatroom-button').click(); view.querySelector('.configure-chatroom-button').click();
const IQs = _converse.connection.IQ_stanzas; const IQs = _converse.connection.IQ_stanzas;
let iq = await u.waitUntil(() => _.filter( const s = `iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_OWNER}"]`;
IQs, let iq = await u.waitUntil(() => IQs.filter(iq => iq.querySelector(s)).pop());
iq => iq.querySelector(
`iq[to="${jid}"] query[xmlns="${Strophe.NS.MUC_OWNER}"]`
)).pop());
const response_el = u.toStanza( const response_el = u.toStanza(
`<iq xmlns="jabber:client" `<iq xmlns="jabber:client"
@ -2303,13 +2303,13 @@ describe("Groupchats", function () {
</query> </query>
</iq>`); </iq>`);
_converse.connection._dataRecv(mock.createRequest(response_el)); _converse.connection._dataRecv(mock.createRequest(response_el));
const el = await u.waitUntil(() => document.querySelector('.chatroom-form legend')); await u.waitUntil(() => document.querySelector('.chatroom-form input'));
expect(el.textContent.trim()).toBe("Configuration for room@conference.example.org"); expect(view.querySelector('.chatroom-form legend').textContent.trim()).toBe("Configuration for room@conference.example.org");
sizzle('[name="muc#roomconfig_membersonly"]', view).pop().click(); sizzle('[name="muc#roomconfig_membersonly"]', view).pop().click();
sizzle('[name="muc#roomconfig_roomname"]', view).pop().value = "New room name" sizzle('[name="muc#roomconfig_roomname"]', view).pop().value = "New room name"
view.querySelector('.chatroom-form input[type="submit"]').click(); view.querySelector('.chatroom-form input[type="submit"]').click();
iq = await u.waitUntil(() => _.filter(IQs, iq => u.matchesSelector(iq, `iq[to="${jid}"][type="set"]`)).pop()); iq = await u.waitUntil(() => IQs.filter(iq => u.matchesSelector(iq, `iq[to="${muc_jid}"][type="set"]`)).pop());
const result = $iq({ const result = $iq({
"xmlns": "jabber:client", "xmlns": "jabber:client",
"type": "result", "type": "result",
@ -2321,14 +2321,13 @@ describe("Groupchats", function () {
IQs.length = 0; // Empty the array IQs.length = 0; // Empty the array
_converse.connection._dataRecv(mock.createRequest(result)); _converse.connection._dataRecv(mock.createRequest(result));
iq = await u.waitUntil(() => _.filter( iq = await u.waitUntil(() => IQs.filter(
IQs,
iq => iq.querySelector( iq => iq.querySelector(
`iq[to="${jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
)).pop()); )).pop());
const features_stanza = $iq({ const features_stanza = $iq({
'from': jid, 'from': muc_jid,
'id': iq.getAttribute('id'), 'id': iq.getAttribute('id'),
'to': 'romeo@montague.lit/desktop', 'to': 'romeo@montague.lit/desktop',
'type': 'result' 'type': 'result'
@ -2360,6 +2359,11 @@ describe("Groupchats", function () {
_converse.connection._dataRecv(mock.createRequest(features_stanza)); _converse.connection._dataRecv(mock.createRequest(features_stanza));
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();
modal = _converse.api.modal.get('muc-details-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
features_list = modal.el.querySelector('.features-list'); features_list = modal.el.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(
@ -2906,8 +2910,7 @@ describe("Groupchats", function () {
}); });
_converse.connection.IQ_stanzas = []; _converse.connection.IQ_stanzas = [];
_converse.connection._dataRecv(mock.createRequest(result)); _converse.connection._dataRecv(mock.createRequest(result));
iq_stanza = await u.waitUntil(() => _.filter( iq_stanza = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
_converse.connection.IQ_stanzas,
iq => iq.querySelector('iq[to="lounge@muc.montague.lit"][type="get"] item[affiliation="member"]')).pop() iq => iq.querySelector('iq[to="lounge@muc.montague.lit"][type="get"] item[affiliation="member"]')).pop()
); );
@ -2930,8 +2933,7 @@ describe("Groupchats", function () {
_converse.connection._dataRecv(mock.createRequest(result)); _converse.connection._dataRecv(mock.createRequest(result));
expect(view.model.occupants.length).toBe(2); expect(view.model.occupants.length).toBe(2);
iq_stanza = await u.waitUntil(() => _.filter( iq_stanza = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
_converse.connection.IQ_stanzas,
iq => iq.querySelector('iq[to="lounge@muc.montague.lit"][type="get"] item[affiliation="owner"]')).pop() iq => iq.querySelector('iq[to="lounge@muc.montague.lit"][type="get"] item[affiliation="owner"]')).pop()
); );
@ -2954,8 +2956,7 @@ describe("Groupchats", function () {
_converse.connection._dataRecv(mock.createRequest(result)); _converse.connection._dataRecv(mock.createRequest(result));
expect(view.model.occupants.length).toBe(2); expect(view.model.occupants.length).toBe(2);
iq_stanza = await u.waitUntil(() => _.filter( iq_stanza = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
_converse.connection.IQ_stanzas,
iq => iq.querySelector('iq[to="lounge@muc.montague.lit"][type="get"] item[affiliation="admin"]')).pop() iq => iq.querySelector('iq[to="lounge@muc.montague.lit"][type="get"] item[affiliation="admin"]')).pop()
); );
@ -3721,7 +3722,6 @@ describe("Groupchats", function () {
const muc_jid = 'protected'; const muc_jid = 'protected';
await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo'); await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo');
const view = _converse.chatboxviews.get(muc_jid); const view = _converse.chatboxviews.get(muc_jid);
spyOn(view, 'renderPasswordForm').and.callThrough();
const presence = $pres().attrs({ const presence = $pres().attrs({
'from': `${muc_jid}/romeo`, 'from': `${muc_jid}/romeo`,
@ -3735,8 +3735,7 @@ describe("Groupchats", function () {
_converse.connection._dataRecv(mock.createRequest(presence)); _converse.connection._dataRecv(mock.createRequest(presence));
const chat_body = view.querySelector('.chatroom-body'); const chat_body = view.querySelector('.chatroom-body');
expect(view.renderPasswordForm).toHaveBeenCalled(); await u.waitUntil(() => chat_body.querySelectorAll('form.chatroom-form').length === 1);
expect(chat_body.querySelectorAll('form.chatroom-form').length).toBe(1);
expect(chat_body.querySelector('.chatroom-form label').textContent.trim()) expect(chat_body.querySelector('.chatroom-form label').textContent.trim())
.toBe('This groupchat requires a password'); .toBe('This groupchat requires a password');
@ -3755,8 +3754,7 @@ describe("Groupchats", function () {
const muc_jid = 'members-only@muc.montague.lit' const muc_jid = 'members-only@muc.montague.lit'
await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo'); await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo');
const view = _converse.chatboxviews.get(muc_jid); const view = _converse.chatboxviews.get(muc_jid);
const iq = await u.waitUntil(() => _.filter( const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
_converse.connection.IQ_stanzas,
iq => iq.querySelector( iq => iq.querySelector(
`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
)).pop()); )).pop());
@ -3791,8 +3789,8 @@ describe("Groupchats", function () {
.c('registration-required').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; .c('registration-required').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
_converse.connection._dataRecv(mock.createRequest(presence)); _converse.connection._dataRecv(mock.createRequest(presence));
expect(view.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim()) await u.waitUntil(() => view.querySelector('.chatroom-body converse-muc-disconnected .disconnect-msg:last-child')?.textContent?.trim() ===
.toBe('You are not on the member list of this groupchat.'); 'You are not on the member list of this groupchat.');
done(); done();
})); }));
@ -3802,8 +3800,7 @@ describe("Groupchats", function () {
const muc_jid = 'off-limits@muc.montague.lit' const muc_jid = 'off-limits@muc.montague.lit'
await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo'); await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo');
const iq = await u.waitUntil(() => _.filter( const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
_converse.connection.IQ_stanzas,
iq => iq.querySelector( iq => iq.querySelector(
`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
)).pop()); )).pop());
@ -3834,8 +3831,8 @@ describe("Groupchats", function () {
.c('forbidden').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; .c('forbidden').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
_converse.connection._dataRecv(mock.createRequest(presence)); _converse.connection._dataRecv(mock.createRequest(presence));
expect(view.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim()) const el = await u.waitUntil(() => view.querySelector('.chatroom-body converse-muc-disconnected .disconnect-msg:last-child'));
.toBe('You have been banned from this groupchat.'); expect(el.textContent.trim()).toBe('You have been banned from this groupchat.');
done(); done();
})); }));
@ -3844,8 +3841,7 @@ describe("Groupchats", function () {
const muc_jid = 'conflicted@muc.montague.lit'; const muc_jid = 'conflicted@muc.montague.lit';
await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo'); await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo');
const iq = await u.waitUntil(() => _.filter( const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
_converse.connection.IQ_stanzas,
iq => iq.querySelector( iq => iq.querySelector(
`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
)).pop()); )).pop());
@ -3876,8 +3872,8 @@ describe("Groupchats", function () {
.c('conflict').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; .c('conflict').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
_converse.connection._dataRecv(mock.createRequest(presence)); _converse.connection._dataRecv(mock.createRequest(presence));
expect(view.querySelector('.muc-nickname-form .validation-message').textContent.trim()) const el = await u.waitUntil(() => view.querySelector('.muc-nickname-form .validation-message'));
.toBe('The nickname you chose is reserved or currently in use, please choose a different one.'); expect(el.textContent.trim()).toBe('The nickname you chose is reserved or currently in use, please choose a different one.');
done(); done();
})); }));
@ -3948,8 +3944,7 @@ describe("Groupchats", function () {
await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo') await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo')
// We pretend this is a new room, so no disco info is returned. // We pretend this is a new room, so no disco info is returned.
const iq = await u.waitUntil(() => _.filter( const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
_converse.connection.IQ_stanzas,
iq => iq.querySelector( iq => iq.querySelector(
`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
)).pop()); )).pop());
@ -3974,8 +3969,8 @@ describe("Groupchats", function () {
.c('error').attrs({by:'lounge@montague.lit', type:'cancel'}) .c('error').attrs({by:'lounge@montague.lit', type:'cancel'})
.c('not-allowed').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; .c('not-allowed').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
_converse.connection._dataRecv(mock.createRequest(presence)); _converse.connection._dataRecv(mock.createRequest(presence));
expect(view.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim()) const el = await u.waitUntil(() => view.querySelector('.chatroom-body converse-muc-disconnected .disconnect-msg:last-child'));
.toBe('You are not allowed to create new groupchats.'); expect(el.textContent.trim()).toBe('You are not allowed to create new groupchats.');
done(); done();
})); }));
@ -3985,8 +3980,7 @@ describe("Groupchats", function () {
const muc_jid = 'conformist@muc.montague.lit' const muc_jid = 'conformist@muc.montague.lit'
await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo'); await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo');
const iq = await u.waitUntil(() => _.filter( const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
_converse.connection.IQ_stanzas,
iq => iq.querySelector( iq => iq.querySelector(
`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
)).pop()); )).pop());
@ -4013,8 +4007,8 @@ describe("Groupchats", function () {
.c('not-acceptable').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; .c('not-acceptable').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
_converse.connection._dataRecv(mock.createRequest(presence)); _converse.connection._dataRecv(mock.createRequest(presence));
expect(view.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim()) const el = await u.waitUntil(() => view.querySelector('.chatroom-body converse-muc-disconnected .disconnect-msg:last-child'));
.toBe("Your nickname doesn't conform to this groupchat's policies."); expect(el.textContent.trim()).toBe("Your nickname doesn't conform to this groupchat's policies.");
done(); done();
})); }));
@ -4024,8 +4018,7 @@ describe("Groupchats", function () {
const muc_jid = 'nonexistent@muc.montague.lit' const muc_jid = 'nonexistent@muc.montague.lit'
await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo'); await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo');
const iq = await u.waitUntil(() => _.filter( const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
_converse.connection.IQ_stanzas,
iq => iq.querySelector( iq => iq.querySelector(
`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
)).pop()); )).pop());
@ -4052,8 +4045,8 @@ describe("Groupchats", function () {
.c('item-not-found').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; .c('item-not-found').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
_converse.connection._dataRecv(mock.createRequest(presence)); _converse.connection._dataRecv(mock.createRequest(presence));
expect(view.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim()) const el = await u.waitUntil(() => view.querySelector('.chatroom-body converse-muc-disconnected .disconnect-msg:last-child'));
.toBe("This groupchat does not (yet) exist."); expect(el.textContent.trim()).toBe("This groupchat does not (yet) exist.");
done(); done();
})); }));
@ -4063,8 +4056,7 @@ describe("Groupchats", function () {
const muc_jid = 'maxed-out@muc.montague.lit' const muc_jid = 'maxed-out@muc.montague.lit'
await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo') await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo')
const iq = await u.waitUntil(() => _.filter( const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
_converse.connection.IQ_stanzas,
iq => iq.querySelector( iq => iq.querySelector(
`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` `iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
)).pop()); )).pop());
@ -4091,8 +4083,8 @@ describe("Groupchats", function () {
.c('service-unavailable').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; .c('service-unavailable').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
_converse.connection._dataRecv(mock.createRequest(presence)); _converse.connection._dataRecv(mock.createRequest(presence));
expect(view.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim()) const el = await u.waitUntil(() => view.querySelector('.chatroom-body converse-muc-disconnected .disconnect-msg:last-child'));
.toBe("This groupchat has reached its maximum number of participants."); expect(el.textContent.trim()).toBe("This groupchat has reached its maximum number of participants.");
done(); done();
})); }));
}); });

View File

@ -37,7 +37,9 @@ converse.MUC_TRAFFIC_STATES_LIST = Object.values(converse.MUC_TRAFFIC_STATES);
converse.MUC_ROLE_CHANGES = { OP: 'op', DEOP: 'deop', VOICE: 'voice', MUTE: 'mute' }; converse.MUC_ROLE_CHANGES = { OP: 'op', DEOP: 'deop', VOICE: 'voice', MUTE: 'mute' };
converse.MUC_ROLE_CHANGES_LIST = Object.values(converse.MUC_ROLE_CHANGES); converse.MUC_ROLE_CHANGES_LIST = Object.values(converse.MUC_ROLE_CHANGES);
converse.MUC_INFO_CODES = { converse.MUC = {};
converse.MUC.INFO_CODES = {
'visibility_changes': ['100', '102', '103', '172', '173', '174'], 'visibility_changes': ['100', '102', '103', '172', '173', '174'],
'self': ['110'], 'self': ['110'],
'non_privacy_changes': ['104', '201'], 'non_privacy_changes': ['104', '201'],
@ -241,15 +243,15 @@ converse.plugins.add('converse-muc', {
'muc_nickname_from_jid': false, 'muc_nickname_from_jid': false,
'muc_send_probes': false, 'muc_send_probes': false,
'muc_show_info_messages': [ 'muc_show_info_messages': [
...converse.MUC_INFO_CODES.visibility_changes, ...converse.MUC.INFO_CODES.visibility_changes,
...converse.MUC_INFO_CODES.self, ...converse.MUC.INFO_CODES.self,
...converse.MUC_INFO_CODES.non_privacy_changes, ...converse.MUC.INFO_CODES.non_privacy_changes,
...converse.MUC_INFO_CODES.muc_logging_changes, ...converse.MUC.INFO_CODES.muc_logging_changes,
...converse.MUC_INFO_CODES.nickname_changes, ...converse.MUC.INFO_CODES.nickname_changes,
...converse.MUC_INFO_CODES.disconnect_messages, ...converse.MUC.INFO_CODES.disconnect_messages,
...converse.MUC_INFO_CODES.affiliation_changes, ...converse.MUC.INFO_CODES.affiliation_changes,
...converse.MUC_INFO_CODES.join_leave_events, ...converse.MUC.INFO_CODES.join_leave_events,
...converse.MUC_INFO_CODES.role_changes ...converse.MUC.INFO_CODES.role_changes
], ],
'muc_show_logs_before_join': false, 'muc_show_logs_before_join': false,
'muc_show_ogp_unfurls': true, 'muc_show_ogp_unfurls': true,

View File

@ -2576,16 +2576,8 @@ const ChatRoomMixin = {
// Accept default configuration // Accept default configuration
this.sendConfiguration().then(() => this.refreshDiscoInfo()); this.sendConfiguration().then(() => this.refreshDiscoInfo());
} else { } else {
/** this.session.save({ 'view': converse.MUC.VIEWS.CONFIG });
* Triggered when a new room has been created which first needs to be configured return;
* and when `auto_configure` is set to `false`.
* Used by `_converse.ChatRoomView` in order to know when to render the
* configuration form for a new room.
* @event _converse.ChatRoom#configurationNeeded
* @example _converse.api.listen.on('configurationNeeded', () => { ... });
*/
this.trigger('configurationNeeded');
return; // We haven't yet entered the groupchat, so bail here.
} }
} else if (!this.features.get('fetched')) { } else if (!this.features.get('fetched')) {
// The features for this groupchat weren't fetched. // The features for this groupchat weren't fetched.

View File

@ -1,24 +1,29 @@
import tpl_muc_bookmark_form from './templates/form.js'; import tpl_muc_bookmark_form from './templates/form.js';
import { View } from '@converse/skeletor/src/view.js'; import { CustomElement } from 'components/element';
import { _converse } from '@converse/headless/core'; import { _converse, api } from "@converse/headless/core";
const MUCBookmarkForm = View.extend({ class MUCBookmarkForm extends CustomElement {
className: 'muc-bookmark-form chatroom-form-container',
initialize (attrs) { static get properties () {
this.chatroomview = attrs.chatroomview; return {
this.render(); 'jid': { type: String }
}, }
}
toHTML () { connectedCallback () {
super.connectedCallback();
this.model = _converse.chatboxes.get(this.jid);
}
render () {
return tpl_muc_bookmark_form( return tpl_muc_bookmark_form(
Object.assign(this.model.toJSON(), { Object.assign(this.model.toJSON(), {
'onCancel': ev => this.closeBookmarkForm(ev), 'onCancel': ev => this.closeBookmarkForm(ev),
'onSubmit': ev => this.onBookmarkFormSubmitted(ev) 'onSubmit': ev => this.onBookmarkFormSubmitted(ev)
}) })
); );
}, }
onBookmarkFormSubmitted (ev) { onBookmarkFormSubmitted (ev) {
ev.preventDefault(); ev.preventDefault();
@ -29,12 +34,14 @@ const MUCBookmarkForm = View.extend({
'nick': ev.target.querySelector('input[name=nick]')?.value 'nick': ev.target.querySelector('input[name=nick]')?.value
}); });
this.closeBookmarkForm(ev); this.closeBookmarkForm(ev);
}, }
closeBookmarkForm (ev) { closeBookmarkForm (ev) {
ev.preventDefault(); ev.preventDefault();
this.chatroomview.closeForm(); this.model.session.save('view', null);
} }
}); }
api.elements.define('converse-muc-bookmark-form', MUCBookmarkForm);
export default MUCBookmarkForm; export default MUCBookmarkForm;

View File

@ -22,7 +22,6 @@ export const bookmarkableChatRoomView = {
}, },
renderBookmarkForm () { renderBookmarkForm () {
this.hideChatRoomContents();
if (!this.bookmark_form) { if (!this.bookmark_form) {
this.bookmark_form = new _converse.MUCBookmarkForm({ this.bookmark_form = new _converse.MUCBookmarkForm({
'model': this.model, 'model': this.model,
@ -38,7 +37,7 @@ export const bookmarkableChatRoomView = {
ev?.preventDefault(); ev?.preventDefault();
const models = _converse.bookmarks.where({ 'jid': this.model.get('jid') }); const models = _converse.bookmarks.where({ 'jid': this.model.get('jid') });
if (!models.length) { if (!models.length) {
this.renderBookmarkForm(); this.model.session.set('view', converse.MUC.VIEWS.BOOKMARK);
} else { } else {
models.forEach(model => model.destroy()); models.forEach(model => model.destroy());
} }
@ -60,13 +59,13 @@ export const eventMethods = {
} }
}, },
addBookmarkViaEvent (ev) { async addBookmarkViaEvent (ev) {
/* Add a bookmark as determined by the passed in /* Add a bookmark as determined by the passed in
* event. * event.
*/ */
ev.preventDefault(); ev.preventDefault();
const jid = ev.target.getAttribute('data-room-jid'); const jid = ev.target.getAttribute('data-room-jid');
api.rooms.open(jid, { 'bring_to_foreground': true }); const room = await api.rooms.open(jid, { 'bring_to_foreground': true });
_converse.chatboxviews.get(jid).renderBookmarkForm(); room.session.save('view', converse.MUC.VIEWS.BOOKMARK);
} }
} }

View File

@ -153,13 +153,14 @@ export function minimize (ev, model) {
} }
// save the scroll position to restore it on maximize // save the scroll position to restore it on maximize
const view = _converse.chatboxviews.get(model.get('jid')); const view = _converse.chatboxviews.get(model.get('jid'));
const content = view.querySelector('.chat-content__messages'); const scroll = view.querySelector('.chat-content__messages')?.scrollTop;
const scroll = content.scrollTop; if (scroll) {
if (model.collection && model.collection.browserStorage) { if (model.collection && model.collection.browserStorage) {
model.save({ scroll }); model.save({ scroll });
} else { } else {
model.set({ scroll }); model.set({ scroll });
} }
}
model.setChatState(_converse.INACTIVE); model.setChatState(_converse.INACTIVE);
u.safeSave(model, { u.safeSave(model, {
'hidden': true, 'hidden': true,

View File

@ -1,43 +1,41 @@
import log from "@converse/headless/log"; import log from "@converse/headless/log";
import tpl_muc_config_form from "./templates/muc-config-form.js"; import tpl_muc_config_form from "./templates/muc-config-form.js";
import { View } from '@converse/skeletor/src/view.js'; import { CustomElement } from 'components/element';
import { __ } from 'i18n'; import { __ } from 'i18n';
import { api, converse } from "@converse/headless/core"; import { _converse, api, converse } from "@converse/headless/core";
const { sizzle } = converse.env; const { sizzle } = converse.env;
const u = converse.env.utils; const u = converse.env.utils;
const MUCConfigForm = View.extend({ class MUCConfigForm extends CustomElement {
className: 'chatroom-form-container muc-config-form',
initialize (attrs) { static get properties () {
this.chatroomview = attrs.chatroomview; return {
this.listenTo(this.chatroomview.model.features, 'change:passwordprotected', this.render); 'jid': { type: String }
this.listenTo(this.chatroomview.model.features, 'change:config_stanza', this.render);
this.render();
},
toHTML () {
const stanza = u.toStanza(this.model.get('config_stanza'));
const whitelist = api.settings.get('roomconfig_whitelist');
let fields = sizzle('field', stanza);
if (whitelist.length) {
fields = fields.filter(f => whitelist.includes(f.getAttribute('var')));
} }
const password_protected = this.model.features.get('passwordprotected'); }
const options = {
'new_password': !password_protected, connectedCallback () {
'fixed_username': this.model.get('jid') super.connectedCallback();
}; this.model = _converse.chatboxes.get(this.jid);
this.listenTo(this.model.features, 'change:passwordprotected', this.requestUpdate);
this.listenTo(this.model.session, 'change:config_stanza', this.requestUpdate);
this.getConfig();
}
render () {
return tpl_muc_config_form({ return tpl_muc_config_form({
'closeConfigForm': ev => this.closeConfigForm(ev), 'model': this.model,
'fields': fields.map(f => u.xForm2TemplateResult(f, stanza, options)), 'closeConfigForm': ev => this.closeForm(ev),
'instructions': stanza.querySelector('instructions')?.textContent,
'submitConfigForm': ev => this.submitConfigForm(ev), 'submitConfigForm': ev => this.submitConfigForm(ev),
'title': stanza.querySelector('title')?.textContent
}); });
}, }
async getConfig () {
const iq = await this.model.fetchRoomConfiguration();
this.model.session.set('config_stanza', iq.outerHTML);
}
async submitConfigForm (ev) { async submitConfigForm (ev) {
ev.preventDefault(); ev.preventDefault();
@ -53,13 +51,15 @@ const MUCConfigForm = View.extend({
api.alert('error', __('Error'), message); api.alert('error', __('Error'), message);
} }
await this.model.refreshDiscoInfo(); await this.model.refreshDiscoInfo();
this.chatroomview.closeForm(); this.closeForm();
},
closeConfigForm (ev) {
ev.preventDefault();
this.chatroomview.closeForm();
} }
});
closeForm (ev) {
ev?.preventDefault?.();
this.model.session.set('view', null);
}
}
api.elements.define('converse-muc-config-form', MUCConfigForm);
export default MUCConfigForm export default MUCConfigForm

View File

@ -0,0 +1,38 @@
import tpl_muc_destroyed from './templates/muc-destroyed.js';
import { CustomElement } from 'components/element';
import { _converse, api } from "@converse/headless/core";
class MUCDestroyed extends CustomElement {
static get properties () {
return {
'jid': { type: String }
}
}
connectedCallback () {
super.connectedCallback();
this.model = _converse.chatboxes.get(this.jid);
}
render () {
const reason = this.model.get('destroyed_reason');
const moved_jid = this.model.get('moved_jid');
return tpl_muc_destroyed({
moved_jid,
reason,
'onSwitch': ev => this.onSwitch(ev)
});
}
async onSwitch (ev) {
ev.preventDefault();
const moved_jid = this.model.get('moved_jid');
const room = await api.rooms.get(moved_jid, {}, true);
room.maybeShow(true);
this.model.destroy();
}
}
api.elements.define('converse-muc-destroyed', MUCDestroyed);

View File

@ -0,0 +1,38 @@
import tpl_muc_disconnect from './templates/muc-disconnect.js';
import { CustomElement } from 'components/element';
import { __ } from 'i18n';
import { _converse, api } from "@converse/headless/core";
class MUCDisconnected extends CustomElement {
static get properties () {
return {
'jid': { type: String }
}
}
connectedCallback () {
super.connectedCallback();
this.model = _converse.chatboxes.get(this.jid);
}
render () {
const message = this.model.get('disconnection_message');
if (!message) {
return;
}
const messages = [message];
const actor = this.model.get('disconnection_actor');
if (actor) {
messages.push(__('This action was done by %1$s.', actor));
}
const reason = this.model.get('disconnection_reason');
if (reason) {
messages.push(__('The reason given is: "%1$s".', reason));
}
return tpl_muc_disconnect(messages);
}
}
api.elements.define('converse-muc-disconnected', MUCDisconnected);

View File

@ -56,7 +56,7 @@ export default class MUCHeading extends ChatHeading {
} }
getAndRenderConfigurationForm () { getAndRenderConfigurationForm () {
_converse.chatboxviews.get(this.getAttribute('jid'))?.getAndRenderConfigurationForm(); this.model.session.set('view', converse.MUC.VIEWS.CONFIG);
} }
showModeratorToolsModal () { showModeratorToolsModal () {
@ -86,7 +86,7 @@ export default class MUCHeading extends ChatHeading {
buttons.push({ buttons.push({
'i18n_text': __('Configure'), 'i18n_text': __('Configure'),
'i18n_title': __('Configure this groupchat'), 'i18n_title': __('Configure this groupchat'),
'handler': ev => this.getAndRenderConfigurationForm(ev), 'handler': () => this.getAndRenderConfigurationForm(),
'a_class': 'configure-chatroom-button', 'a_class': 'configure-chatroom-button',
'icon_class': 'fa-wrench', 'icon_class': 'fa-wrench',
'name': 'configure' 'name': 'configure'

View File

@ -19,6 +19,11 @@ import { api, converse, _converse } from '@converse/headless/core';
const { Strophe } = converse.env; const { Strophe } = converse.env;
converse.MUC.VIEWS = {
CONFIG: 'config-form',
BOOKMARK: 'bookmark-form'
}
function setMUCDomain (domain, controlboxview) { function setMUCDomain (domain, controlboxview) {
controlboxview.querySelector('converse-rooms-list') controlboxview.querySelector('converse-rooms-list')
.model.save('muc_domain', Strophe.getDomainFromJid(domain)); .model.save('muc_domain', Strophe.getDomainFromJid(domain));

View File

@ -5,17 +5,10 @@ import BaseChatView from 'shared/chat/baseview.js';
import ModeratorToolsModal from 'modals/moderator-tools.js'; import ModeratorToolsModal from 'modals/moderator-tools.js';
import log from '@converse/headless/log'; import log from '@converse/headless/log';
import tpl_muc from './templates/muc.js'; import tpl_muc from './templates/muc.js';
import tpl_muc_destroyed from './templates/muc-destroyed.js';
import tpl_muc_disconnect from './templates/muc-disconnect.js';
import tpl_muc_nickname_form from './templates/muc-nickname-form.js';
import tpl_spinner from 'templates/spinner.js';
import { Model } from '@converse/skeletor/src/model.js'; import { Model } from '@converse/skeletor/src/model.js';
import { __ } from 'i18n'; import { __ } from 'i18n';
import { _converse, api, converse } from '@converse/headless/core'; import { _converse, api, converse } from '@converse/headless/core';
import { render } from 'lit-html'; import { html, render } from "lit-html";
const { sizzle } = converse.env;
const u = converse.env.utils;
/** /**
* Mixin which turns a ChatBoxView into a ChatRoomView * Mixin which turns a ChatBoxView into a ChatRoomView
@ -25,8 +18,6 @@ const u = converse.env.utils;
*/ */
export default class MUCView extends BaseChatView { export default class MUCView extends BaseChatView {
length = 300 length = 300
tagName = 'div'
className = 'chatbox chatroom hidden'
is_chatroom = true is_chatroom = true
events = { events = {
'click .chatbox-navback': 'showControlBox', 'click .chatbox-navback': 'showControlBox',
@ -34,14 +25,12 @@ export default class MUCView extends BaseChatView {
// Arrow functions don't work here because you can't bind a different `this` param to them. // Arrow functions don't work here because you can't bind a different `this` param to them.
'click .occupant-nick': function (ev) { 'click .occupant-nick': function (ev) {
this.insertIntoTextArea(ev.target.textContent); this.insertIntoTextArea(ev.target.textContent);
}, }
'submit .muc-nickname-form': 'submitNickname'
} }
async initialize () { async initialize () {
const jid = this.getAttribute('jid'); const jid = this.getAttribute('jid');
_converse.chatboxviews.add(jid, this); _converse.chatboxviews.add(jid, this);
this.model = _converse.chatboxes.get(jid); this.model = _converse.chatboxes.get(jid);
this.initDebounced(); this.initDebounced();
@ -49,18 +38,17 @@ export default class MUCView extends BaseChatView {
this.listenTo(this.model, 'change:composing_spoiler', this.renderMessageForm); this.listenTo(this.model, 'change:composing_spoiler', this.renderMessageForm);
this.listenTo(this.model, 'change:hidden', () => this.afterShown()); this.listenTo(this.model, 'change:hidden', () => this.afterShown());
this.listenTo(this.model, 'change:minimized', () => this.afterShown()); this.listenTo(this.model, 'change:minimized', () => this.afterShown());
this.listenTo(this.model, 'configurationNeeded', this.getAndRenderConfigurationForm);
this.listenTo(this.model, 'show', this.show); this.listenTo(this.model, 'show', this.show);
this.listenTo(this.model.session, 'change:connection_status', this.renderAfterTransition); this.listenTo(this.model.session, 'change:connection_status', this.updateAfterTransition);
this.listenTo(this.model.session, 'change:view', this.render);
await this.render(); await this.render();
// Need to be registered after render has been called. // Need to be registered after render has been called.
this.listenTo(this.model.messages, 'add', this.onMessageAdded); this.listenTo(this.model.messages, 'add', this.onMessageAdded);
this.listenTo(this.model.occupants, 'change:show', this.showJoinOrLeaveNotification); this.listenTo(this.model.occupants, 'change:show', this.showJoinOrLeaveNotification);
this.listenTo(this.model.occupants, 'remove', this.onOccupantRemoved);
this.renderAfterTransition(); this.updateAfterTransition();
this.model.maybeShow(); this.model.maybeShow();
this.scrollDown(); this.scrollDown();
/** /**
@ -76,47 +64,20 @@ export default class MUCView extends BaseChatView {
this.setAttribute('id', this.model.get('box_id')); this.setAttribute('id', this.model.get('box_id'));
render( render(
tpl_muc({ tpl_muc({
'chatview': this, 'getNicknameRequiredTemplate': () => this.getNicknameRequiredTemplate(),
'conn_status': this.model.session.get('connection_status'),
'model': this.model, 'model': this.model,
'occupants': this.model.occupants,
'show_sidebar':
!this.model.get('hidden_occupants') &&
this.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED,
'markScrolled': ev => this.markScrolled(ev),
'muc_show_logs_before_join': api.settings.get('muc_show_logs_before_join'),
'show_send_button': _converse.show_send_button
}), }),
this this
); );
this.notifications = this.querySelector('.chat-content__notifications'); this.notifications = this.querySelector('.chat-content__notifications');
this.help_container = this.querySelector('.chat-content__help'); this.help_container = this.querySelector('.chat-content__help');
if (
!api.settings.get('muc_show_logs_before_join') &&
this.model.session.get('connection_status') !== converse.ROOMSTATUS.ENTERED
) {
this.showSpinner();
}
// Render header as late as possible since it's async and we // Render header as late as possible since it's async and we
// want the rest of the DOM elements to be available ASAP. // want the rest of the DOM elements to be available ASAP.
// Otherwise e.g. this.notifications is not yet defined when accessed elsewhere. // Otherwise e.g. this.notifications is not yet defined when accessed elsewhere.
!this.model.get('hidden') && this.show(); !this.model.get('hidden') && this.show();
} }
/**
* Get the nickname value from the form and then join the groupchat with it.
* @private
* @method _converse.ChatRoomView#submitNickname
* @param { Event }
*/
submitNickname (ev) {
ev.preventDefault();
const nick = ev.target.nick.value.trim();
nick && this.model.join(nick);
}
showModeratorToolsModal (affiliation) { showModeratorToolsModal (affiliation) {
if (!this.model.verifyRoles(['moderator'])) { if (!this.model.verifyRoles(['moderator'])) {
return; return;
@ -197,247 +158,28 @@ export default class MUCView extends BaseChatView {
} }
} }
/** getNicknameRequiredTemplate () {
* Renders a form given an IQ stanza containing the current const jid = this.model.get('jid');
* groupchat configuration.
* Returns a promise which resolves once the user has
* either submitted the form, or canceled it.
* @private
* @method _converse.ChatRoomView#renderConfigurationForm
* @param { XMLElement } stanza: The IQ stanza containing the groupchat config.
*/
renderConfigurationForm (stanza) {
this.hideChatRoomContents();
this.model.save('config_stanza', stanza.outerHTML);
if (!this.config_form) {
this.config_form = new _converse.MUCConfigForm({
'model': this.model,
'chatroomview': this
});
const container_el = this.querySelector('.chatroom-body');
container_el.insertAdjacentElement('beforeend', this.config_form.el);
}
u.showElement(this.config_form.el);
}
/**
* Renders a form which allows the user to choose theirnickname.
* @private
* @method _converse.ChatRoomView#renderNicknameForm
*/
renderNicknameForm () {
if (api.settings.get('muc_show_logs_before_join')) { if (api.settings.get('muc_show_logs_before_join')) {
this.hideSpinner(); return html`<converse-muc-chatarea jid="${jid}"></converse-muc-chatarea>`;
u.showElement(this.querySelector('converse-muc-chatarea'));
} else { } else {
const form = this.querySelector('.muc-nickname-form'); return html`<converse-muc-nickname-form jid="${jid}"></converse-muc-nickname-form>`;
const tpl_result = tpl_muc_nickname_form(this.model.toJSON());
const form_el = u.getElementFromTemplateResult(tpl_result);
if (form) {
sizzle('.spinner', this).forEach(u.removeElement);
form.outerHTML = form_el.outerHTML;
} else {
this.hideChatRoomContents();
const container = this.querySelector('.chatroom-body');
container.insertAdjacentElement('beforeend', form_el);
}
}
u.safeSave(this.model.session, { 'connection_status': converse.ROOMSTATUS.NICKNAME_REQUIRED });
}
/**
* Remove the configuration form without submitting and return to the chat view.
* @private
* @method _converse.ChatRoomView#closeForm
*/
closeForm () {
sizzle('.chatroom-form-container', this).forEach(e => u.addClass('hidden', e));
this.renderAfterTransition();
}
/**
* Start the process of configuring a groupchat, either by
* rendering a configuration form, or by auto-configuring
* based on the "roomconfig" data stored on the
* {@link _converse.ChatRoom}.
* Stores the new configuration on the {@link _converse.ChatRoom}
* once completed.
* @private
* @method _converse.ChatRoomView#getAndRenderConfigurationForm
* @param { Event } ev - DOM event that might be passed in if this
* method is called due to a user action. In this
* case, auto-configure won't happen, regardless of
* the settings.
*/
getAndRenderConfigurationForm () {
if (!this.config_form || !u.isVisible(this.config_form.el)) {
this.showSpinner();
this.model
.fetchRoomConfiguration()
.then(iq => this.renderConfigurationForm(iq))
.catch(e => log.error(e));
} else {
this.closeForm();
} }
} }
hideChatRoomContents () { updateAfterTransition () {
const container_el = this.querySelector('.chatroom-body'); const conn_status = this.model.session.get('connection_status');
if (container_el !== null) { if (conn_status === converse.ROOMSTATUS.CONNECTING) {
[].forEach.call(container_el.children, child => child.classList.add('hidden'));
}
}
renderPasswordForm () {
this.hideChatRoomContents();
const message = this.model.get('password_validation_message');
this.model.save('password_validation_message', undefined);
if (!this.password_form) {
this.password_form = new _converse.MUCPasswordForm({
'model': new Model({
'validation_message': message
}),
'chatroomview': this
});
const container_el = this.querySelector('.chatroom-body');
container_el.insertAdjacentElement('beforeend', this.password_form.el);
} else {
this.password_form.model.set('validation_message', message);
}
u.showElement(this.password_form.el);
this.model.session.save('connection_status', converse.ROOMSTATUS.PASSWORD_REQUIRED);
}
showDestroyedMessage () {
u.hideElement(this.querySelector('converse-muc-chatarea'));
sizzle('.spinner', this).forEach(u.removeElement);
const reason = this.model.get('destroyed_reason');
const moved_jid = this.model.get('moved_jid');
this.model.save({
'destroyed_reason': undefined,
'moved_jid': undefined
});
const container = this.querySelector('.disconnect-container');
render(tpl_muc_destroyed(moved_jid, reason), container);
const switch_el = container.querySelector('a.switch-chat');
if (switch_el) {
switch_el.addEventListener('click', async ev => {
ev.preventDefault();
const room = await api.rooms.get(moved_jid, null, true);
room.maybeShow(true);
this.model.destroy();
});
}
u.showElement(container);
}
showDisconnectMessage () {
const message = this.model.get('disconnection_message');
if (!message) {
return;
}
u.hideElement(this.querySelector('converse-muc-chatarea'));
sizzle('.spinner', this).forEach(u.removeElement);
const messages = [message];
const actor = this.model.get('disconnection_actor');
if (actor) {
messages.push(__('This action was done by %1$s.', actor));
}
const reason = this.model.get('disconnection_reason');
if (reason) {
messages.push(__('The reason given is: "%1$s".', reason));
}
this.model.save({ this.model.save({
'disconnection_actor': undefined,
'disconnection_message': undefined, 'disconnection_message': undefined,
'disconnection_reason': undefined, 'disconnection_reason': undefined,
'disconnection_actor': undefined 'moved_jid': undefined,
'password_validation_message': undefined,
'reason': undefined,
}); });
const container = this.querySelector('.disconnect-container');
render(tpl_muc_disconnect(messages), container);
u.showElement(container);
} }
this.render();
/**
* Working backwards, get today's most recent join/leave notification
* from the same user (if any exists) after the most recent chat message.
* @private
* @method _converse.ChatRoomView#getPreviousJoinOrLeaveNotification
* @param {HTMLElement} el
* @param {string} nick
*/
getPreviousJoinOrLeaveNotification (el, nick) { // eslint-disable-line class-methods-use-this
const today = new Date().toISOString().split('T')[0];
while (el !== null) {
if (!el.classList.contains('chat-info')) {
return;
}
// Check whether el is still from today.
// We don't use `Dayjs.same` here, since it's about 4 times slower.
const date = el.getAttribute('data-isodate');
if (date && date.split('T')[0] !== today) {
return;
}
const data = el?.dataset || {};
if (data.join === nick || data.leave === nick || data.leavejoin === nick || data.joinleave === nick) {
return el;
}
el = el.previousElementSibling;
}
}
/**
* Rerender the groupchat after some kind of transition. For
* example after the spinner has been removed or after a
* form has been submitted and removed.
* @private
* @method _converse.ChatRoomView#renderAfterTransition
*/
renderAfterTransition () {
const conn_status = this.model.session.get('connection_status');
if (conn_status === converse.ROOMSTATUS.NICKNAME_REQUIRED) {
this.renderNicknameForm();
} else if (conn_status === converse.ROOMSTATUS.PASSWORD_REQUIRED) {
this.renderPasswordForm();
} else if (conn_status === converse.ROOMSTATUS.CONNECTING) {
this.showSpinner();
} else if (conn_status === converse.ROOMSTATUS.ENTERED) {
this.hideSpinner();
this.hideChatRoomContents();
u.showElement(this.querySelector('converse-muc-chatarea'));
this.scrollDown();
this.maybeFocus();
} else if (conn_status === converse.ROOMSTATUS.DISCONNECTED) {
this.showDisconnectMessage();
} else if (conn_status === converse.ROOMSTATUS.DESTROYED) {
this.showDestroyedMessage();
}
}
showSpinner () {
sizzle('.spinner', this).forEach(u.removeElement);
this.hideChatRoomContents();
const container_el = this.querySelector('.chatroom-body');
container_el.insertAdjacentElement('afterbegin', u.getElementFromTemplateResult(tpl_spinner()));
}
/**
* Check if the spinner is being shown and if so, hide it.
* Also make sure then that the chat area and occupants
* list are both visible.
* @private
* @method _converse.ChatRoomView#hideSpinner
*/
hideSpinner () {
const spinner = this.querySelector('.spinner');
if (spinner !== null) {
u.removeElement(spinner);
this.renderAfterTransition();
}
return this;
} }
} }

View File

@ -0,0 +1,25 @@
import tpl_muc_nickname_form from './templates/muc-nickname-form.js';
import { CustomElement } from 'components/element';
import { _converse, api } from "@converse/headless/core";
class MUCNicknameForm extends CustomElement {
static get properties () {
return {
'jid': { type: String }
}
}
connectedCallback () {
super.connectedCallback();
this.model = _converse.chatboxes.get(this.jid);
}
render () {
return tpl_muc_nickname_form(this.model);
}
}
api.elements.define('converse-muc-nickname-form', MUCNicknameForm);
export default MUCNicknameForm;

View File

@ -1,30 +1,39 @@
import tpl_muc_password_form from "./templates/muc-password-form.js"; import tpl_muc_password_form from "./templates/muc-password-form.js";
import { View } from '@converse/skeletor/src/view.js'; import { CustomElement } from 'components/element';
import { _converse, api } from "@converse/headless/core";
const MUCPasswordForm = View.extend({ class MUCPasswordForm extends CustomElement {
className: 'chatroom-form-container muc-password-form',
initialize (attrs) { static get properties () {
this.chatroomview = attrs.chatroomview; return {
this.listenTo(this.model, 'change:validation_message', this.render); 'jid': { type: String }
}
}
connectedCallback () {
super.connectedCallback();
this.model = _converse.chatboxes.get(this.jid);
this.listenTo(this.model, 'change:password_validation_message', this.render);
this.render(); this.render();
}, }
toHTML () { render () {
return tpl_muc_password_form({ return tpl_muc_password_form({
'jid': this.model.get('jid'), 'jid': this.model.get('jid'),
'submitPassword': ev => this.submitPassword(ev), 'submitPassword': ev => this.submitPassword(ev),
'validation_message': this.model.get('validation_message') 'validation_message': this.model.get('password_validation_message')
}); });
}, }
submitPassword (ev) { submitPassword (ev) {
ev.preventDefault(); ev.preventDefault();
const password = this.el.querySelector('input[type=password]').value; const password = this.querySelector('input[type=password]').value;
this.chatroomview.model.join(this.chatroomview.model.get('nick'), password); this.model.join(this.model.get('nick'), password);
this.model.set('validation_message', null); this.model.set('password_validation_message', null);
} }
}); }
api.elements.define('converse-muc-password-form', MUCPasswordForm);
export default MUCPasswordForm; export default MUCPasswordForm;

View File

@ -16,7 +16,7 @@ export default (o) => {
return (o.can_edit) ? tpl_can_edit() : html`<span class="muc-bottom-panel muc-bottom-panel--muted">${i18n_not_allowed}</span>`; return (o.can_edit) ? tpl_can_edit() : html`<span class="muc-bottom-panel muc-bottom-panel--muted">${i18n_not_allowed}</span>`;
} else if (conn_status == converse.ROOMSTATUS.NICKNAME_REQUIRED) { } else if (conn_status == converse.ROOMSTATUS.NICKNAME_REQUIRED) {
if (api.settings.get('muc_show_logs_before_join')) { if (api.settings.get('muc_show_logs_before_join')) {
return html`<span class="muc-bottom-panel muc-bottom-panel--nickname">${tpl_muc_nickname_form(o.model.toJSON())}</span>`; return html`<span class="muc-bottom-panel muc-bottom-panel--nickname">${tpl_muc_nickname_form(o.model)}</span>`;
} }
} else { } else {
return ''; return '';

View File

@ -1,20 +1,51 @@
import { html } from "lit-html"; import tpl_spinner from 'templates/spinner.js';
import { __ } from 'i18n'; import { __ } from 'i18n';
import { api, converse } from "@converse/headless/core";
import { html } from "lit-html";
const { sizzle } = converse.env;
const u = converse.env.utils;
export default (o) => { export default (o) => {
const whitelist = api.settings.get('roomconfig_whitelist');
const config_stanza = o.model.session.get('config_stanza');
let fields = [];
let instructions = '';
let title;
if (config_stanza) {
const stanza = u.toStanza(config_stanza);
fields = sizzle('field', stanza);
if (whitelist.length) {
fields = fields.filter(f => whitelist.includes(f.getAttribute('var')));
}
const password_protected = o.model.features.get('passwordprotected');
const options = {
'new_password': !password_protected,
'fixed_username': o.model.get('jid')
};
fields = fields.map(f => u.xForm2TemplateResult(f, stanza, options));
instructions = stanza.querySelector('instructions')?.textContent;
title = stanza.querySelector('title')?.textContent;
} else {
title = __('Loading configuration form');
}
const i18n_save = __('Save'); const i18n_save = __('Save');
const i18n_cancel = __('Cancel'); const i18n_cancel = __('Cancel');
return html` return html`
<form class="converse-form chatroom-form" autocomplete="off" @submit=${o.submitConfigForm}> <form class="converse-form chatroom-form ${fields.length ? '' : 'converse-form--spinner'}"
autocomplete="off"
@submit=${o.submitConfigForm}>
<fieldset class="form-group"> <fieldset class="form-group">
<legend>${o.title}</legend> <legend class="centered">${title}</legend>
${ (o.title !== o.instructions) ? html`<p class="form-help">${o.instructions}</p>` : '' } ${ (title !== instructions) ? html`<p class="form-help">${instructions}</p>` : '' }
${ o.fields } ${ fields.length ? fields : tpl_spinner({'classes': 'hor_centered'}) }
</fieldset> </fieldset>
${ fields.length ? html`
<fieldset> <fieldset>
<input type="submit" class="btn btn-primary" value="${i18n_save}"> <input type="submit" class="btn btn-primary" value="${i18n_save}">
<input type="button" class="btn btn-secondary button-cancel" value="${i18n_cancel}" @click=${o.closeConfigForm}> <input type="button" class="btn btn-secondary button-cancel" value="${i18n_cancel}" @click=${o.closeConfigForm}>
</fieldset> </fieldset>` : '' }
</form> </form>
`; `;
} }

View File

@ -1,20 +1,23 @@
import { __ } from 'i18n'; import { __ } from 'i18n';
import { html } from "lit-html"; import { html } from "lit-html";
const tpl_moved = (o) => {
const tpl_moved = (jid) => { const i18n_moved = __('The conversation has moved to a new address. Click the link below to enter.');
const i18n_moved = __('The conversation has moved. Click below to enter.');
return html` return html`
<p class="moved-label">${i18n_moved}</p> <p class="moved-label">${i18n_moved}</p>
<p class="moved-link"><a class="switch-chat" href="#">${jid}</a></p>`; <p class="moved-link">
<a class="switch-chat" @click=${ev => o.onSwitch(ev)}>${o.moved_jid}</a>
</p>`;
} }
export default (jid, reason) => { export default (o) => {
const i18n_non_existent = __('This groupchat no longer exists'); const i18n_non_existent = __('This groupchat no longer exists');
const i18n_reason = __('The following reason was given: "%1$s"', o.reason || '');
return html` return html`
<div class="alert alert-danger"> <div class="alert alert-danger">
<h3 class="alert-heading disconnect-msg">${i18n_non_existent}</h3> <h3 class="alert-heading disconnect-msg">${i18n_non_existent}</h3>
${ reason ? html`<p class="destroyed-reason">"${reason}"</p>` : '' } </div>
${ jid ? tpl_moved(jid) : '' } ${ o.reason ? html`<p class="destroyed-reason">${i18n_reason}</p>` : '' }
</div>`; ${ o.moved_jid ? tpl_moved(o) : '' }
`;
} }

View File

@ -2,25 +2,33 @@ import { __ } from 'i18n';
import { api } from "@converse/headless/core"; import { api } from "@converse/headless/core";
import { html } from "lit-html"; import { html } from "lit-html";
function submitNickname (ev, model) {
ev.preventDefault();
const nick = ev.target.nick.value.trim();
nick && model.join(nick);
}
export default (o) => { export default (model) => {
const i18n_nickname = __('Nickname'); const i18n_nickname = __('Nickname');
const i18n_join = __('Enter groupchat'); const i18n_join = __('Enter groupchat');
const i18n_heading = api.settings.get('muc_show_logs_before_join') ? const i18n_heading = api.settings.get('muc_show_logs_before_join') ?
__('Choose a nickname to enter') : __('Choose a nickname to enter') :
__('Please choose your nickname'); __('Please choose your nickname');
const validation_message = 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"
@submit=${ev => submitNickname(ev, model)}>
<form class="converse-form chatroom-form converse-centered-form"> <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">${o.nickname_validation_message}</p> <p class="validation-message">${validation_message}</p>
<input type="text" <input type="text"
required="required" required="required"
name="nick" name="nick"
value="${o.nick || ''}" value="${model.get('nick') || ''}"
class="form-control ${o.nickname_validation_message ? 'error': ''}" class="form-control ${validation_message ? 'error': ''}"
placeholder="${i18n_nickname}"/> placeholder="${i18n_nickname}"/>
</fieldset> </fieldset>
<fieldset class="form-group"> <fieldset class="form-group">

View File

@ -1,15 +1,45 @@
import '../chatarea.js';
import '../bottom-panel.js'; import '../bottom-panel.js';
import '../chatarea.js';
import '../config-form.js';
import '../destroyed.js';
import '../disconnected.js';
import '../heading.js'; import '../heading.js';
import '../nickname-form.js';
import '../password-form.js';
import '../sidebar.js'; import '../sidebar.js';
import tpl_spinner from 'templates/spinner.js';
import { converse } from "@converse/headless/core";
import { html } from "lit-html"; import { html } from "lit-html";
export default (o) => html`
function getChatRoomBody (o) {
const view = o.model.session.get('view');
const jid = o.model.get('jid');
const RS = converse.ROOMSTATUS;
const conn_status = o.model.session.get('connection_status');
if (view === converse.MUC.VIEWS.CONFIG) {
return html`<converse-muc-config-form class="muc-form-container" jid="${jid}"></converse-muc-config-form>`;
} else if (view === converse.MUC.VIEWS.BOOKMARK) {
return html`<converse-muc-bookmark-form class="muc-form-container" jid="${jid}"></converse-muc-bookmark-form>`;
} else {
return html`
${ conn_status == RS.PASSWORD_REQUIRED ? html`<converse-muc-password-form class="muc-form-container" jid="${jid}"></converse-muc-password-form>` : '' }
${ conn_status == RS.ENTERED ? html`<converse-muc-chatarea jid="${jid}"></converse-muc-chatarea>` : '' }
${ conn_status == RS.CONNECTING ? tpl_spinner() : '' }
${ conn_status == RS.NICKNAME_REQUIRED ? o.getNicknameRequiredTemplate() : '' }
${ conn_status == RS.DISCONNECTED ? html`<converse-muc-disconnected jid="${jid}"></converse-muc-disconnected>` : '' }
${ conn_status == RS.DESTROYED ? html`<converse-muc-destroyed jid="${jid}"></converse-muc-destroyed>` : '' }
`;
}
}
export default (o) => {
return html`
<div class="flyout box-flyout"> <div class="flyout box-flyout">
<converse-dragresize></converse-dragresize> <converse-dragresize></converse-dragresize>
<converse-muc-heading jid="${o.model.get('jid')}" class="chat-head chat-head-chatroom row no-gutters"></converse-muc-heading> <converse-muc-heading jid="${o.model.get('jid')}" class="chat-head chat-head-chatroom row no-gutters"></converse-muc-heading>
<div class="chat-body chatroom-body row no-gutters"> <div class="chat-body chatroom-body row no-gutters">${getChatRoomBody(o)}</div>
<converse-muc-chatarea jid="${o.model.get('jid')}"></converse-muc-chatarea>
</div> </div>
</div> `;
`; }

View File

@ -20,7 +20,6 @@ const COMMAND_TO_ROLE = {
'voice': 'participant' 'voice': 'participant'
}; };
export function getAutoCompleteListItem (text, input) { export function getAutoCompleteListItem (text, input) {
input = input.trim(); input = input.trim();
const element = document.createElement('li'); const element = document.createElement('li');