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 {
.badge--muc {
@ -324,7 +336,7 @@
}
}
}
.chatroom-form-container {
.muc-form-container {
background-color: white;
border: 0;
color: var(--text-color);
@ -376,7 +388,7 @@
padding: 0;
height: 16em;
.chatroom-form-container {
.muc-form-container {
.chatroom-form {
padding-top: 2em;
padding-bottom: 0;
@ -528,7 +540,7 @@
}
.chatroom-body {
height: 100%;
.chatroom-form-container {
.muc-form-container {
height: 100%;
position: relative;
}

View File

@ -118,6 +118,10 @@
padding-bottom: 0;
}
&.converse-form--spinner {
height: 100%;
}
&.converse-centered-form {
min-height: 66%;
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 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);
const toggle = view.querySelector('.toggle-bookmark');
expect(toggle.title).toBe('Bookmark this groupchat');
toggle.click();
expect(view.renderBookmarkForm).toHaveBeenCalled();
view.querySelector('.button-cancel').click();
expect(view.closeForm).toHaveBeenCalled();
const cancel_button = await u.waitUntil(() => view.querySelector('.button-cancel'));
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(toggle.title).toBe('Bookmark this groupchat');
toggle.click();
expect(view.renderBookmarkForm).toHaveBeenCalled();
/* Client uploads data:
* --------------------
@ -75,13 +74,13 @@ describe("A chat room", function () {
* </iq>
*/
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="autojoin"]').checked = 'checked';
form.querySelector('input[name="nick"]').value = 'JC';
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(
() => 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 () {},
'keyCode': 13 // Enter
}
spyOn(_converse.connection, 'send');
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter_event);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
const msg = _converse.connection.send.calls.all()[0].args[0];
expect(msg.toLocaleString())
.toBe(`<message from="romeo@montague.lit/orchard" id="${msg.nodeTree.getAttribute("id")}" `+
const sent_stanzas = _converse.connection.sent_stanzas;
const msg = await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName.toLowerCase() === 'message').pop());
expect(Strophe.serialize(msg))
.toBe(`<message from="romeo@montague.lit/orchard" id="${msg.getAttribute("id")}" `+
`to="lounge@montague.lit" type="groupchat" `+
`xmlns="jabber:client">`+
`<body>hello Link Mauve</body>`+
`<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"/>`+
`<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>`);
done();
}));
@ -374,7 +374,6 @@ describe("A sent groupchat message", function () {
'stopPropagation': function stopPropagation () {},
'keyCode': 13 // Enter
}
spyOn(_converse.connection, 'send');
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter_event);
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?'
);
const msg = _converse.connection.send.calls.all()[0].args[0];
expect(msg.toLocaleString())
.toBe(`<message from="romeo@montague.lit/orchard" id="${msg.nodeTree.getAttribute("id")}" `+
const sent_stanzas = _converse.connection.sent_stanzas;
const msg = await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName.toLowerCase() === 'message').pop());
expect(Strophe.serialize(msg))
.toBe(`<message from="romeo@montague.lit/orchard" id="${msg.getAttribute("id")}" `+
`to="lounge@montague.lit" type="groupchat" `+
`xmlns="jabber:client">`+
`<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="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"/>`+
`<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>`);
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.querySelectorAll('.chat-msg').length).toBe(1);
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?';
bottom_panel.onKeyDown(enter_event);
await u.waitUntil(() => view.querySelector('.chat-msg__text').textContent ===
'hello z3r0 gibson sw0rdf1sh, how are you?', 500);
const correction = _converse.connection.send.calls.all()[1].args[0];
expect(correction.toLocaleString())
.toBe(`<message from="romeo@montague.lit/orchard" id="${correction.nodeTree.getAttribute("id")}" `+
const correction = sent_stanzas.filter(s => s.nodeName.toLowerCase() === 'message').pop();
expect(Strophe.serialize(correction))
.toBe(`<message from="romeo@montague.lit/orchard" id="${correction.getAttribute("id")}" `+
`to="lounge@montague.lit" type="groupchat" `+
`xmlns="jabber:client">`+
`<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="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"/>`+
`<replace id="${msg.nodeTree.getAttribute("id")}" xmlns="urn:xmpp:message-correct:0"/>`+
`<origin-id id="${correction.nodeTree.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+
`<replace id="${msg.getAttribute("id")}" xmlns="urn:xmpp:message-correct:0"/>`+
`<origin-id id="${correction.querySelector('origin-id').getAttribute("id")}" xmlns="urn:xmpp:sid:0"/>`+
`</message>`);
done();
}));

View File

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

View File

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

View File

@ -147,7 +147,6 @@ describe("Groupchats", function () {
return done();
}));
it("maintains its state across reloads",
mock.initConverse([], {
'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>`+
`</query>`+
`</iq>`);
done();
}));
@ -567,7 +565,7 @@ describe("Groupchats", function () {
const muc_jid = 'lounge@montague.lit';
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',
nick = mock.chatroom_names[0],
msg = $msg({
@ -577,18 +575,19 @@ describe("Groupchats", function () {
'type': 'groupchat'
}).c('body').t(message).tree();
await view.model.handleMessageStanza(msg);
await view.model.close();
await model.handleMessageStanza(msg);
await u.waitUntil(() => document.querySelector('converse-chat-message'));
await model.close();
await u.waitUntil(() => !document.querySelector('converse-chat-message'));
_converse.connection.IQ_stanzas = [];
await mock.openAndEnterChatRoom(_converse, muc_jid , 'romeo');
await u.waitUntil(() => view.querySelector('converse-chat-message'));
expect(view.model.messages.length).toBe(1);
expect(view.querySelectorAll('converse-chat-message').length).toBe(1);
await u.waitUntil(() => document.querySelector('converse-chat-message'));
expect(model.messages.length).toBe(1);
expect(document.querySelectorAll('converse-chat-message').length).toBe(1);
done()
}));
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) {
@ -734,7 +733,7 @@ describe("Groupchats", function () {
.c('status', {code: '110'});
_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");
await room_creation_promise;
@ -1228,6 +1227,10 @@ describe("Groupchats", function () {
.c('status', {code: '110'});
_converse.connection._dataRecv(mock.createRequest(presence));
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();
/* Check that an IQ is sent out, asking for the
@ -1470,7 +1473,8 @@ describe("Groupchats", function () {
view.model.rejoin();
// Test that members aren't removed when we reconnect
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();
}));
@ -1607,12 +1611,12 @@ describe("Groupchats", function () {
const view = _converse.chatboxviews.get('problematic@muc.montague.lit');
_converse.connection._dataRecv(mock.createRequest(presence));
expect(view.querySelector('.chatroom-body .disconnect-msg').textContent.trim())
.toBe('This groupchat no longer exists');
const msg = await u.waitUntil(() => view.querySelector('.chatroom-body .disconnect-msg'));
expect(msg.textContent.trim()).toBe('This groupchat no longer exists');
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())
.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())
.toBe(`other-room@chat.jabberfr.org`);
done();
@ -1649,11 +1653,10 @@ describe("Groupchats", function () {
* node='x-roomuser-item'/>
* </iq>
*/
const iq = await u.waitUntil(() => _.filter(
IQ_stanzas,
const iq = await u.waitUntil(() => IQ_stanzas.filter(
s => sizzle(`iq[to="${muc_jid}"] query[node="x-roomuser-item"]`, s).length
).pop()
);
).pop());
expect(Strophe.serialize(iq)).toBe(
`<iq from="romeo@montague.lit/orchard" id="${iq.getAttribute('id')}" to="lounge@montague.lit" `+
`type="get" xmlns="jabber:client">`+
@ -2116,8 +2119,7 @@ describe("Groupchats", function () {
const muc_jid = 'coven@chat.shakespeare.lit';
await _converse.api.rooms.open(muc_jid, { nick });
const stanza = await u.waitUntil(() => _.filter(
IQ_stanzas,
const stanza = await u.waitUntil(() => IQ_stanzas.filter(
iq => iq.querySelector(
`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
)).pop());
@ -2195,13 +2197,13 @@ describe("Groupchats", function () {
'muc_unmoderated',
'muc_nonanonymous'
];
await mock.openAndEnterChatRoom(_converse, 'room@conference.example.org', 'romeo', features);
const jid = 'room@conference.example.org';
const view = _converse.chatboxviews.get(jid);
const muc_jid = 'room@conference.example.org';
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', features);
const view = _converse.chatboxviews.get(muc_jid);
const info_el = view.querySelector(".show-muc-details-modal");
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);
let features_list = modal.el.querySelector('.features-list');
@ -2228,14 +2230,12 @@ describe("Groupchats", function () {
expect(view.model.features.get('unsecured')).toBe(false);
await u.waitUntil(() => view.querySelector('.chatbox-title__text').textContent.trim() === 'Room');
modal.el.querySelector('.close').click();
view.querySelector('.configure-chatroom-button').click();
const IQs = _converse.connection.IQ_stanzas;
let iq = await u.waitUntil(() => _.filter(
IQs,
iq => iq.querySelector(
`iq[to="${jid}"] query[xmlns="${Strophe.NS.MUC_OWNER}"]`
)).pop());
const s = `iq[to="${muc_jid}"] query[xmlns="${Strophe.NS.MUC_OWNER}"]`;
let iq = await u.waitUntil(() => IQs.filter(iq => iq.querySelector(s)).pop());
const response_el = u.toStanza(
`<iq xmlns="jabber:client"
@ -2303,13 +2303,13 @@ describe("Groupchats", function () {
</query>
</iq>`);
_converse.connection._dataRecv(mock.createRequest(response_el));
const el = await u.waitUntil(() => document.querySelector('.chatroom-form legend'));
expect(el.textContent.trim()).toBe("Configuration for room@conference.example.org");
await u.waitUntil(() => document.querySelector('.chatroom-form input'));
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_roomname"]', view).pop().value = "New room name"
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({
"xmlns": "jabber:client",
"type": "result",
@ -2321,14 +2321,13 @@ describe("Groupchats", function () {
IQs.length = 0; // Empty the array
_converse.connection._dataRecv(mock.createRequest(result));
iq = await u.waitUntil(() => _.filter(
IQs,
iq = await u.waitUntil(() => IQs.filter(
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());
const features_stanza = $iq({
'from': jid,
'from': muc_jid,
'id': iq.getAttribute('id'),
'to': 'romeo@montague.lit/desktop',
'type': 'result'
@ -2360,6 +2359,11 @@ describe("Groupchats", function () {
_converse.connection._dataRecv(mock.createRequest(features_stanza));
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_shown = features_list.textContent.split('\n').map(s => s.trim()).filter(s => s);
expect(features_shown.join(' ')).toBe(
@ -2906,8 +2910,7 @@ describe("Groupchats", function () {
});
_converse.connection.IQ_stanzas = [];
_converse.connection._dataRecv(mock.createRequest(result));
iq_stanza = await u.waitUntil(() => _.filter(
_converse.connection.IQ_stanzas,
iq_stanza = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
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));
expect(view.model.occupants.length).toBe(2);
iq_stanza = await u.waitUntil(() => _.filter(
_converse.connection.IQ_stanzas,
iq_stanza = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
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));
expect(view.model.occupants.length).toBe(2);
iq_stanza = await u.waitUntil(() => _.filter(
_converse.connection.IQ_stanzas,
iq_stanza = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
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';
await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo');
const view = _converse.chatboxviews.get(muc_jid);
spyOn(view, 'renderPasswordForm').and.callThrough();
const presence = $pres().attrs({
'from': `${muc_jid}/romeo`,
@ -3735,8 +3735,7 @@ describe("Groupchats", function () {
_converse.connection._dataRecv(mock.createRequest(presence));
const chat_body = view.querySelector('.chatroom-body');
expect(view.renderPasswordForm).toHaveBeenCalled();
expect(chat_body.querySelectorAll('form.chatroom-form').length).toBe(1);
await u.waitUntil(() => chat_body.querySelectorAll('form.chatroom-form').length === 1);
expect(chat_body.querySelector('.chatroom-form label').textContent.trim())
.toBe('This groupchat requires a password');
@ -3755,8 +3754,7 @@ describe("Groupchats", function () {
const muc_jid = 'members-only@muc.montague.lit'
await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo');
const view = _converse.chatboxviews.get(muc_jid);
const iq = await u.waitUntil(() => _.filter(
_converse.connection.IQ_stanzas,
const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
iq => iq.querySelector(
`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
)).pop());
@ -3791,8 +3789,8 @@ describe("Groupchats", function () {
.c('registration-required').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
_converse.connection._dataRecv(mock.createRequest(presence));
expect(view.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim())
.toBe('You are not on the member list of this groupchat.');
await u.waitUntil(() => view.querySelector('.chatroom-body converse-muc-disconnected .disconnect-msg:last-child')?.textContent?.trim() ===
'You are not on the member list of this groupchat.');
done();
}));
@ -3802,8 +3800,7 @@ describe("Groupchats", function () {
const muc_jid = 'off-limits@muc.montague.lit'
await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo');
const iq = await u.waitUntil(() => _.filter(
_converse.connection.IQ_stanzas,
const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
iq => iq.querySelector(
`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
)).pop());
@ -3834,8 +3831,8 @@ describe("Groupchats", function () {
.c('forbidden').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
_converse.connection._dataRecv(mock.createRequest(presence));
expect(view.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim())
.toBe('You have been banned from this groupchat.');
const el = await u.waitUntil(() => view.querySelector('.chatroom-body converse-muc-disconnected .disconnect-msg:last-child'));
expect(el.textContent.trim()).toBe('You have been banned from this groupchat.');
done();
}));
@ -3844,8 +3841,7 @@ describe("Groupchats", function () {
const muc_jid = 'conflicted@muc.montague.lit';
await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo');
const iq = await u.waitUntil(() => _.filter(
_converse.connection.IQ_stanzas,
const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
iq => iq.querySelector(
`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
)).pop());
@ -3876,8 +3872,8 @@ describe("Groupchats", function () {
.c('conflict').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
_converse.connection._dataRecv(mock.createRequest(presence));
expect(view.querySelector('.muc-nickname-form .validation-message').textContent.trim())
.toBe('The nickname you chose is reserved or currently in use, please choose a different one.');
const el = await u.waitUntil(() => view.querySelector('.muc-nickname-form .validation-message'));
expect(el.textContent.trim()).toBe('The nickname you chose is reserved or currently in use, please choose a different one.');
done();
}));
@ -3948,8 +3944,7 @@ describe("Groupchats", function () {
await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo')
// We pretend this is a new room, so no disco info is returned.
const iq = await u.waitUntil(() => _.filter(
_converse.connection.IQ_stanzas,
const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
iq => iq.querySelector(
`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
)).pop());
@ -3974,8 +3969,8 @@ describe("Groupchats", function () {
.c('error').attrs({by:'lounge@montague.lit', type:'cancel'})
.c('not-allowed').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
_converse.connection._dataRecv(mock.createRequest(presence));
expect(view.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim())
.toBe('You are not allowed to create new groupchats.');
const el = await u.waitUntil(() => view.querySelector('.chatroom-body converse-muc-disconnected .disconnect-msg:last-child'));
expect(el.textContent.trim()).toBe('You are not allowed to create new groupchats.');
done();
}));
@ -3985,8 +3980,7 @@ describe("Groupchats", function () {
const muc_jid = 'conformist@muc.montague.lit'
await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo');
const iq = await u.waitUntil(() => _.filter(
_converse.connection.IQ_stanzas,
const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
iq => iq.querySelector(
`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
)).pop());
@ -4013,8 +4007,8 @@ describe("Groupchats", function () {
.c('not-acceptable').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
_converse.connection._dataRecv(mock.createRequest(presence));
expect(view.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim())
.toBe("Your nickname doesn't conform to this groupchat's policies.");
const el = await u.waitUntil(() => view.querySelector('.chatroom-body converse-muc-disconnected .disconnect-msg:last-child'));
expect(el.textContent.trim()).toBe("Your nickname doesn't conform to this groupchat's policies.");
done();
}));
@ -4024,8 +4018,7 @@ describe("Groupchats", function () {
const muc_jid = 'nonexistent@muc.montague.lit'
await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo');
const iq = await u.waitUntil(() => _.filter(
_converse.connection.IQ_stanzas,
const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
iq => iq.querySelector(
`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
)).pop());
@ -4052,8 +4045,8 @@ describe("Groupchats", function () {
.c('item-not-found').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
_converse.connection._dataRecv(mock.createRequest(presence));
expect(view.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim())
.toBe("This groupchat does not (yet) exist.");
const el = await u.waitUntil(() => view.querySelector('.chatroom-body converse-muc-disconnected .disconnect-msg:last-child'));
expect(el.textContent.trim()).toBe("This groupchat does not (yet) exist.");
done();
}));
@ -4063,8 +4056,7 @@ describe("Groupchats", function () {
const muc_jid = 'maxed-out@muc.montague.lit'
await mock.openChatRoomViaModal(_converse, muc_jid, 'romeo')
const iq = await u.waitUntil(() => _.filter(
_converse.connection.IQ_stanzas,
const iq = await u.waitUntil(() => _converse.connection.IQ_stanzas.filter(
iq => iq.querySelector(
`iq[to="${muc_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]`
)).pop());
@ -4091,8 +4083,8 @@ describe("Groupchats", function () {
.c('service-unavailable').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree;
_converse.connection._dataRecv(mock.createRequest(presence));
expect(view.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent.trim())
.toBe("This groupchat has reached its maximum number of participants.");
const el = await u.waitUntil(() => view.querySelector('.chatroom-body converse-muc-disconnected .disconnect-msg:last-child'));
expect(el.textContent.trim()).toBe("This groupchat has reached its maximum number of participants.");
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_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'],
'self': ['110'],
'non_privacy_changes': ['104', '201'],
@ -241,15 +243,15 @@ converse.plugins.add('converse-muc', {
'muc_nickname_from_jid': false,
'muc_send_probes': false,
'muc_show_info_messages': [
...converse.MUC_INFO_CODES.visibility_changes,
...converse.MUC_INFO_CODES.self,
...converse.MUC_INFO_CODES.non_privacy_changes,
...converse.MUC_INFO_CODES.muc_logging_changes,
...converse.MUC_INFO_CODES.nickname_changes,
...converse.MUC_INFO_CODES.disconnect_messages,
...converse.MUC_INFO_CODES.affiliation_changes,
...converse.MUC_INFO_CODES.join_leave_events,
...converse.MUC_INFO_CODES.role_changes
...converse.MUC.INFO_CODES.visibility_changes,
...converse.MUC.INFO_CODES.self,
...converse.MUC.INFO_CODES.non_privacy_changes,
...converse.MUC.INFO_CODES.muc_logging_changes,
...converse.MUC.INFO_CODES.nickname_changes,
...converse.MUC.INFO_CODES.disconnect_messages,
...converse.MUC.INFO_CODES.affiliation_changes,
...converse.MUC.INFO_CODES.join_leave_events,
...converse.MUC.INFO_CODES.role_changes
],
'muc_show_logs_before_join': false,
'muc_show_ogp_unfurls': true,

View File

@ -2576,16 +2576,8 @@ const ChatRoomMixin = {
// Accept default configuration
this.sendConfiguration().then(() => this.refreshDiscoInfo());
} else {
/**
* Triggered when a new room has been created which first needs to be configured
* 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.
this.session.save({ 'view': converse.MUC.VIEWS.CONFIG });
return;
}
} else if (!this.features.get('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 { View } from '@converse/skeletor/src/view.js';
import { _converse } from '@converse/headless/core';
import { CustomElement } from 'components/element';
import { _converse, api } from "@converse/headless/core";
const MUCBookmarkForm = View.extend({
className: 'muc-bookmark-form chatroom-form-container',
class MUCBookmarkForm extends CustomElement {
initialize (attrs) {
this.chatroomview = attrs.chatroomview;
this.render();
},
static get properties () {
return {
'jid': { type: String }
}
}
toHTML () {
connectedCallback () {
super.connectedCallback();
this.model = _converse.chatboxes.get(this.jid);
}
render () {
return tpl_muc_bookmark_form(
Object.assign(this.model.toJSON(), {
'onCancel': ev => this.closeBookmarkForm(ev),
'onSubmit': ev => this.onBookmarkFormSubmitted(ev)
})
);
},
}
onBookmarkFormSubmitted (ev) {
ev.preventDefault();
@ -29,12 +34,14 @@ const MUCBookmarkForm = View.extend({
'nick': ev.target.querySelector('input[name=nick]')?.value
});
this.closeBookmarkForm(ev);
},
}
closeBookmarkForm (ev) {
ev.preventDefault();
this.chatroomview.closeForm();
this.model.session.save('view', null);
}
});
}
api.elements.define('converse-muc-bookmark-form', MUCBookmarkForm);
export default MUCBookmarkForm;

View File

@ -22,7 +22,6 @@ export const bookmarkableChatRoomView = {
},
renderBookmarkForm () {
this.hideChatRoomContents();
if (!this.bookmark_form) {
this.bookmark_form = new _converse.MUCBookmarkForm({
'model': this.model,
@ -38,7 +37,7 @@ export const bookmarkableChatRoomView = {
ev?.preventDefault();
const models = _converse.bookmarks.where({ 'jid': this.model.get('jid') });
if (!models.length) {
this.renderBookmarkForm();
this.model.session.set('view', converse.MUC.VIEWS.BOOKMARK);
} else {
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
* event.
*/
ev.preventDefault();
const jid = ev.target.getAttribute('data-room-jid');
api.rooms.open(jid, { 'bring_to_foreground': true });
_converse.chatboxviews.get(jid).renderBookmarkForm();
const room = await api.rooms.open(jid, { 'bring_to_foreground': true });
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
const view = _converse.chatboxviews.get(model.get('jid'));
const content = view.querySelector('.chat-content__messages');
const scroll = content.scrollTop;
const scroll = view.querySelector('.chat-content__messages')?.scrollTop;
if (scroll) {
if (model.collection && model.collection.browserStorage) {
model.save({ scroll });
} else {
model.set({ scroll });
}
}
model.setChatState(_converse.INACTIVE);
u.safeSave(model, {
'hidden': true,

View File

@ -1,43 +1,41 @@
import log from "@converse/headless/log";
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 { api, converse } from "@converse/headless/core";
import { _converse, api, converse } from "@converse/headless/core";
const { sizzle } = converse.env;
const u = converse.env.utils;
const MUCConfigForm = View.extend({
className: 'chatroom-form-container muc-config-form',
class MUCConfigForm extends CustomElement {
initialize (attrs) {
this.chatroomview = attrs.chatroomview;
this.listenTo(this.chatroomview.model.features, 'change:passwordprotected', this.render);
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')));
static get properties () {
return {
'jid': { type: String }
}
const password_protected = this.model.features.get('passwordprotected');
const options = {
'new_password': !password_protected,
'fixed_username': this.model.get('jid')
};
}
connectedCallback () {
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({
'closeConfigForm': ev => this.closeConfigForm(ev),
'fields': fields.map(f => u.xForm2TemplateResult(f, stanza, options)),
'instructions': stanza.querySelector('instructions')?.textContent,
'model': this.model,
'closeConfigForm': ev => this.closeForm(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) {
ev.preventDefault();
@ -53,13 +51,15 @@ const MUCConfigForm = View.extend({
api.alert('error', __('Error'), message);
}
await this.model.refreshDiscoInfo();
this.chatroomview.closeForm();
},
closeConfigForm (ev) {
ev.preventDefault();
this.chatroomview.closeForm();
this.closeForm();
}
});
closeForm (ev) {
ev?.preventDefault?.();
this.model.session.set('view', null);
}
}
api.elements.define('converse-muc-config-form', 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 () {
_converse.chatboxviews.get(this.getAttribute('jid'))?.getAndRenderConfigurationForm();
this.model.session.set('view', converse.MUC.VIEWS.CONFIG);
}
showModeratorToolsModal () {
@ -86,7 +86,7 @@ export default class MUCHeading extends ChatHeading {
buttons.push({
'i18n_text': __('Configure'),
'i18n_title': __('Configure this groupchat'),
'handler': ev => this.getAndRenderConfigurationForm(ev),
'handler': () => this.getAndRenderConfigurationForm(),
'a_class': 'configure-chatroom-button',
'icon_class': 'fa-wrench',
'name': 'configure'

View File

@ -19,6 +19,11 @@ import { api, converse, _converse } from '@converse/headless/core';
const { Strophe } = converse.env;
converse.MUC.VIEWS = {
CONFIG: 'config-form',
BOOKMARK: 'bookmark-form'
}
function setMUCDomain (domain, controlboxview) {
controlboxview.querySelector('converse-rooms-list')
.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 log from '@converse/headless/log';
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 { __ } from 'i18n';
import { _converse, api, converse } from '@converse/headless/core';
import { render } from 'lit-html';
const { sizzle } = converse.env;
const u = converse.env.utils;
import { html, render } from "lit-html";
/**
* Mixin which turns a ChatBoxView into a ChatRoomView
@ -25,8 +18,6 @@ const u = converse.env.utils;
*/
export default class MUCView extends BaseChatView {
length = 300
tagName = 'div'
className = 'chatbox chatroom hidden'
is_chatroom = true
events = {
'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.
'click .occupant-nick': function (ev) {
this.insertIntoTextArea(ev.target.textContent);
},
'submit .muc-nickname-form': 'submitNickname'
}
}
async initialize () {
const jid = this.getAttribute('jid');
_converse.chatboxviews.add(jid, this);
this.model = _converse.chatboxes.get(jid);
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:hidden', () => 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.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();
// Need to be registered after render has been called.
this.listenTo(this.model.messages, 'add', this.onMessageAdded);
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.scrollDown();
/**
@ -76,47 +64,20 @@ export default class MUCView extends BaseChatView {
this.setAttribute('id', this.model.get('box_id'));
render(
tpl_muc({
'chatview': this,
'conn_status': this.model.session.get('connection_status'),
'getNicknameRequiredTemplate': () => this.getNicknameRequiredTemplate(),
'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.notifications = this.querySelector('.chat-content__notifications');
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
// want the rest of the DOM elements to be available ASAP.
// Otherwise e.g. this.notifications is not yet defined when accessed elsewhere.
!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) {
if (!this.model.verifyRoles(['moderator'])) {
return;
@ -197,247 +158,28 @@ export default class MUCView extends BaseChatView {
}
}
/**
* Renders a form given an IQ stanza containing the current
* 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 () {
getNicknameRequiredTemplate () {
const jid = this.model.get('jid');
if (api.settings.get('muc_show_logs_before_join')) {
this.hideSpinner();
u.showElement(this.querySelector('converse-muc-chatarea'));
return html`<converse-muc-chatarea jid="${jid}"></converse-muc-chatarea>`;
} else {
const form = this.querySelector('.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();
return html`<converse-muc-nickname-form jid="${jid}"></converse-muc-nickname-form>`;
}
}
hideChatRoomContents () {
const container_el = this.querySelector('.chatroom-body');
if (container_el !== null) {
[].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));
}
updateAfterTransition () {
const conn_status = this.model.session.get('connection_status');
if (conn_status === converse.ROOMSTATUS.CONNECTING) {
this.model.save({
'disconnection_actor': undefined,
'disconnection_message': 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);
}
/**
* 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;
this.render();
}
}

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 { View } from '@converse/skeletor/src/view.js';
import { CustomElement } from 'components/element';
import { _converse, api } from "@converse/headless/core";
const MUCPasswordForm = View.extend({
className: 'chatroom-form-container muc-password-form',
class MUCPasswordForm extends CustomElement {
initialize (attrs) {
this.chatroomview = attrs.chatroomview;
this.listenTo(this.model, 'change:validation_message', this.render);
static get properties () {
return {
'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();
},
}
toHTML () {
render () {
return tpl_muc_password_form({
'jid': this.model.get('jid'),
'submitPassword': ev => this.submitPassword(ev),
'validation_message': this.model.get('validation_message')
'validation_message': this.model.get('password_validation_message')
});
},
}
submitPassword (ev) {
ev.preventDefault();
const password = this.el.querySelector('input[type=password]').value;
this.chatroomview.model.join(this.chatroomview.model.get('nick'), password);
this.model.set('validation_message', null);
const password = this.querySelector('input[type=password]').value;
this.model.join(this.model.get('nick'), password);
this.model.set('password_validation_message', null);
}
});
}
api.elements.define('converse-muc-password-form', 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>`;
} else if (conn_status == converse.ROOMSTATUS.NICKNAME_REQUIRED) {
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 {
return '';

View File

@ -1,20 +1,51 @@
import { html } from "lit-html";
import tpl_spinner from 'templates/spinner.js';
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) => {
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_cancel = __('Cancel');
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">
<legend>${o.title}</legend>
${ (o.title !== o.instructions) ? html`<p class="form-help">${o.instructions}</p>` : '' }
${ o.fields }
<legend class="centered">${title}</legend>
${ (title !== instructions) ? html`<p class="form-help">${instructions}</p>` : '' }
${ fields.length ? fields : tpl_spinner({'classes': 'hor_centered'}) }
</fieldset>
${ fields.length ? html`
<fieldset>
<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}>
</fieldset>
</fieldset>` : '' }
</form>
`;
}

View File

@ -1,20 +1,23 @@
import { __ } from 'i18n';
import { html } from "lit-html";
const tpl_moved = (jid) => {
const i18n_moved = __('The conversation has moved. Click below to enter.');
const tpl_moved = (o) => {
const i18n_moved = __('The conversation has moved to a new address. Click the link below to enter.');
return html`
<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_reason = __('The following reason was given: "%1$s"', o.reason || '');
return html`
<div class="alert alert-danger">
<h3 class="alert-heading disconnect-msg">${i18n_non_existent}</h3>
${ reason ? html`<p class="destroyed-reason">"${reason}"</p>` : '' }
${ jid ? tpl_moved(jid) : '' }
</div>`;
</div>
${ o.reason ? html`<p class="destroyed-reason">${i18n_reason}</p>` : '' }
${ o.moved_jid ? tpl_moved(o) : '' }
`;
}

View File

@ -2,25 +2,33 @@ import { __ } from 'i18n';
import { api } from "@converse/headless/core";
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_join = __('Enter groupchat');
const i18n_heading = api.settings.get('muc_show_logs_before_join') ?
__('Choose a nickname to enter') :
__('Please choose your nickname');
const validation_message = model.get('nickname_validation_message');
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">
<fieldset class="form-group">
<label>${i18n_heading}</label>
<p class="validation-message">${o.nickname_validation_message}</p>
<p class="validation-message">${validation_message}</p>
<input type="text"
required="required"
name="nick"
value="${o.nick || ''}"
class="form-control ${o.nickname_validation_message ? 'error': ''}"
value="${model.get('nick') || ''}"
class="form-control ${validation_message ? 'error': ''}"
placeholder="${i18n_nickname}"/>
</fieldset>
<fieldset class="form-group">

View File

@ -1,15 +1,45 @@
import '../chatarea.js';
import '../bottom-panel.js';
import '../chatarea.js';
import '../config-form.js';
import '../destroyed.js';
import '../disconnected.js';
import '../heading.js';
import '../nickname-form.js';
import '../password-form.js';
import '../sidebar.js';
import tpl_spinner from 'templates/spinner.js';
import { converse } from "@converse/headless/core";
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">
<converse-dragresize></converse-dragresize>
<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">
<converse-muc-chatarea jid="${o.model.get('jid')}"></converse-muc-chatarea>
<div class="chat-body chatroom-body row no-gutters">${getChatRoomBody(o)}</div>
</div>
</div>
`;
`;
}

View File

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