Improvements to rendering MUC MAM logs before joining

- Add config variable `muc_show_logs_before_join`
- Restore nickname form for the original use-case (when not showing MAM logs before joining).
- Render nickname form in the `.muc-bottom-panel` when showing MAM logs (instead of in a modal)
- Show a message when there aren't any archived messages to show
- Fix converse-mam so that chat logs are properly fetched before entering a MUC

closes #1266
This commit is contained in:
JC Brand 2019-11-28 17:01:08 +01:00
parent a83911d67f
commit d7d810ba89
13 changed files with 204 additions and 179 deletions

View File

@ -10,9 +10,10 @@
To add custom emojis, edit the `emojis.json` file.
- Refactor some presence and status handling code from `converse-core` into `@converse/headless/converse-status`.
- New API [\_converse.api.headlines](https://conversejs.org/docs/html/api/-_converse.api.headlines.html#.get)
- New config option [muc-mention-autocomplete-filter](https://conversejs.org/docs/html/configuration.html#muc-mention-autocomplete-filter)
- New config option [muc-mention-autocomplete-show-avatar](https://conversejs.org/docs/html/configuration.html#muc-mention-autocomplete-show-avatar)
- New config option [allow_message_retraction](https://conversejs.org/docs/html/configuration.html#allow-message-retraction)
- New config option [muc-show-logs-before-join](https://conversejs.org/docs/html/configuration.html#muc-show-logs-before-join)
_ New config option [muc_mention_autocomplete_filter](https://conversejs.org/docs/html/configuration.html#muc_mention_autocomplete_filter)
_ New config option [muc_mention_autocomplete_show_avatar](https://conversejs.org/docs/html/configuration.html#muc_mention_autocomplete_show_avatar)
- #129: Add support for XEP-0156: Disovering Alternative XMPP Connection Methods. Only XML is supported for now.
- #1105: Preliminary support for storing persistent data in IndexedDB instead of localStorage

View File

@ -316,6 +316,18 @@
}
}
.empty-history-feedback {
position: relative;
height: 100%;
color: var(--text-color-lighten-15-percent);
span {
width: 100%;
text-align: center;
position: absolute;
margin-top: 50%;
}
}
.muc-bottom-panel {
border-top: var(--message-input-border-top);
height: 3em;
@ -324,6 +336,20 @@
font-size: var(--font-size-small);
background-color: var(--chatroom-head-color);
color: white;
&.muc-bottom-panel--nickname {
padding: 0;
height: 16em;
.chatroom-form-container {
border-radius: 0 !important;
.chatroom-form {
padding-top: 2em;
padding-bottom: 0;
}
}
}
}
.sendXMPPMessage {

View File

@ -196,7 +196,7 @@
</iq>`);
_converse.connection._dataRecv(test_utils.createRequest(result));
await u.waitUntil(() => view.model.messages.length === 5);
expect(view.model.fetchArchivedMessages.calls.count()).toBe(3);
expect(view.model.fetchArchivedMessages.calls.count()).toBe(5);
const msg_els = view.content.querySelectorAll('.chat-msg__text');
expect(Array.from(msg_els).map(e => e.textContent).join(' ')).toBe("2nd Message 3rd Message 4th Message 5th Message 6th Message");
done();

View File

@ -1547,7 +1547,7 @@
expect(presencebroadcast.length).toBe(1);
presencebroadcast[0].value = ['moderator'];
view.el.querySelector('input[type="submit"]').click();
view.el.querySelector('.chatroom-form input[type="submit"]').click();
const sent_stanza = sent_IQ.nodeTree;
expect(sent_stanza.querySelector('field[var="muc#roomconfig_membersonly"] value').textContent.trim()).toBe('1');
@ -2578,7 +2578,7 @@
expect(el.textContent.trim()).toBe("Configuration for room@conference.example.org");
sizzle('[name="muc#roomconfig_membersonly"]', chatroomview.el).pop().click();
sizzle('[name="muc#roomconfig_roomname"]', chatroomview.el).pop().value = "New room name"
chatroomview.el.querySelector('.btn-primary').click();
chatroomview.el.querySelector('.chatroom-form input[type="submit"]').click();
iq = await u.waitUntil(() => _.filter(IQs, iq => u.matchesSelector(iq, `iq[to="${jid}"][type="set"]`)).pop());
const result = $iq({

View File

@ -979,7 +979,7 @@
await u.waitUntil(() => view.el.querySelector('.chat-msg__text').textContent ===
'hello z3r0 gibson sw0rdf1sh, how are you?', 500);
const correction = _converse.connection.send.calls.all()[2].args[0];
const correction = _converse.connection.send.calls.all()[3].args[0];
expect(correction.toLocaleString())
.toBe(`<message from="romeo@montague.lit/orchard" id="${correction.nodeTree.getAttribute("id")}" `+
`to="lounge@montague.lit" type="groupchat" `+

View File

@ -27,7 +27,7 @@ import tpl_chatroom_features from "templates/chatroom_features.html";
import tpl_chatroom_form from "templates/chatroom_form.html";
import tpl_chatroom_head from "templates/chatroom_head.html";
import tpl_chatroom_invite from "templates/chatroom_invite.html";
import tpl_chatroom_nickname_form_modal from "templates/chatroom_nickname_form_modal.html";
import tpl_chatroom_nickname_form from "templates/chatroom_nickname_form.html";
import tpl_chatroom_password_form from "templates/chatroom_password_form.html";
import tpl_chatroom_sidebar from "templates/chatroom_sidebar.html";
import tpl_info from "templates/info.html";
@ -636,18 +636,20 @@ converse.plugins.add('converse-muc-views', {
'click .configure-chatroom-button': 'getAndRenderConfigurationForm',
'click .hide-occupants': 'hideOccupants',
'click .new-msgs-indicator': 'viewUnreadMessages',
'click .occupant-nick': 'onOccupantClicked',
// 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) },
'click .send-button': 'onFormSubmitted',
'click .show-room-details-modal': 'showRoomDetailsModal',
'click .toggle-call': 'toggleCall',
'click .toggle-occupants': 'toggleOccupants',
'click .upload-file': 'toggleFileUpload',
'dragover .chat-textarea': 'onDragOver',
'drop .chat-textarea': 'onDrop',
'input .chat-textarea': 'inputChanged',
'keydown .chat-textarea': 'onKeyDown',
'keyup .chat-textarea': 'onKeyUp',
'paste .chat-textarea': 'onPaste',
'input .chat-textarea': 'inputChanged',
'dragover .chat-textarea': 'onDragOver',
'drop .chat-textarea': 'onDrop',
'submit .muc-nickname-form': 'submitNickname'
},
initialize () {
@ -696,8 +698,8 @@ converse.plugins.add('converse-muc-views', {
this.renderHeading();
this.renderChatArea();
this.renderBottomPanel();
if (this.model.get('connection_status') !== converse.ROOMSTATUS.ENTERED) {
this.showSpinner();
if (!_converse.muc_show_logs_before_join) {
this.model.get('connection_status') !== converse.ROOMSTATUS.ENTERED && this.showSpinner();
}
if (!this.model.get('hidden')) {
this.show();
@ -716,9 +718,11 @@ converse.plugins.add('converse-muc-views', {
renderBottomPanel () {
const container = this.el.querySelector('.bottom-panel');
const can_edit = !(this.model.features.get('moderated') && this.model.getOwnRole() === 'visitor');
container.innerHTML = tpl_chatroom_bottom_panel({__, can_edit});
if (can_edit) {
const entered = this.model.get('connection_status') === converse.ROOMSTATUS.ENTERED;
const can_edit = entered && !(this.model.features.get('moderated') && this.model.getOwnRole() === 'visitor');
const nickname = this.model.get('nickname');
container.innerHTML = tpl_chatroom_bottom_panel({__, can_edit, entered, nickname});
if (entered && can_edit) {
this.renderMessageForm();
this.initMentionAutoComplete();
}
@ -730,7 +734,11 @@ converse.plugins.add('converse-muc-views', {
const container_el = this.el.querySelector('.chatroom-body');
container_el.insertAdjacentHTML(
'beforeend',
tpl_chatarea({'show_send_button': _converse.show_send_button})
tpl_chatarea({
__,
'muc_show_logs_before_join': _converse.muc_show_logs_before_join,
'show_send_button': _converse.show_send_button
})
);
this.content = this.el.querySelector('.chat-content');
}
@ -799,6 +807,21 @@ converse.plugins.add('converse-muc-views', {
this.mention_auto_complete.on('suggestion-box-selectcomplete', () => (this.auto_completing = false));
},
/**
* 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();
if (nick) {
this.model.join(nick);
this.model.set({'nickname': nick});
}
},
onKeyDown (ev) {
if (this.mention_auto_complete.onKeyDown(ev)) {
return;
@ -997,9 +1020,12 @@ converse.plugins.add('converse-muc-views', {
}
},
generateHeadingHTML () {
/* Returns the heading HTML to be rendered.
/**
* Returns the groupchat heading HTML to be rendered.
* @private
* @method _converse.ChatRoomView#generateHeadingHTML
*/
generateHeadingHTML () {
return tpl_chatroom_head(
Object.assign(this.model.toJSON(), {
'isOwner': this.model.getOwnAffiliation() === 'owner',
@ -1028,7 +1054,7 @@ converse.plugins.add('converse-muc-views', {
onConnectionStatusChanged () {
const conn_status = this.model.get('connection_status');
if (conn_status === converse.ROOMSTATUS.NICKNAME_REQUIRED) {
this.renderNicknameButton();
this.renderNicknameForm();
} else if (conn_status === converse.ROOMSTATUS.PASSWORD_REQUIRED) {
this.renderPasswordForm();
} else if (conn_status === converse.ROOMSTATUS.CONNECTING) {
@ -1083,10 +1109,13 @@ converse.plugins.add('converse-muc-views', {
}
},
hideOccupants (ev) {
/* Show or hide the right sidebar containing the chat
/**
* Show or hide the right sidebar containing the chat
* occupants (and the invite widget).
* @private
* @method _converse.ChatRoomView#hideOccupants
*/
hideOccupants (ev) {
if (ev) {
ev.preventDefault();
ev.stopPropagation();
@ -1095,10 +1124,13 @@ converse.plugins.add('converse-muc-views', {
this.scrollDown();
},
toggleOccupants (ev) {
/* Show or hide the right sidebar containing the chat
/**
* Show or hide the right sidebar containing the chat
* occupants (and the invite widget).
* @private
* @method _converse.ChatRoomView#toggleOccupants
*/
toggleOccupants (ev) {
if (ev) {
ev.preventDefault();
ev.stopPropagation();
@ -1107,13 +1139,6 @@ converse.plugins.add('converse-muc-views', {
this.scrollDown();
},
onOccupantClicked (ev) {
/* When an occupant is clicked, insert their nickname into
* the chat textarea input.
*/
this.insertIntoTextArea(ev.target.textContent);
},
verifyRoles (roles, occupant, show_error=true) {
if (!Array.isArray(roles)) {
throw new TypeError('roles must be an Array');
@ -1436,6 +1461,34 @@ converse.plugins.add('converse-muc-views', {
u.showElement(this.config_form.el);
},
/**
* Renders a form which allows the user to choose theirnickname.
* @private
* @method _converse.ChatRoomView#renderNicknameForm
*/
renderNicknameForm () {
const heading = _converse.muc_show_logs_before_join ?
__('Choose a nickname to enter') :
__('Please choose your nickname');
const html = tpl_chatroom_nickname_form({
heading,
'label_nickname': __('Nickname'),
'label_join': __('Enter groupchat'),
'nickname': this.model.get('nickname')
});
if (_converse.muc_show_logs_before_join) {
const container = this.el.querySelector('.muc-bottom-panel');
container.innerHTML = html;
u.addClass('muc-bottom-panel--nickname', container);
} else {
this.hideChatRoomContents();
const container = this.el.querySelector('.chatroom-body');
container.insertAdjacentHTML('beforeend', html);
}
u.safeSave(this.model, {'connection_status': converse.ROOMSTATUS.NICKNAME_REQUIRED});
},
closeForm () {
/* Remove the configuration form without submitting and
* return to the chat view.
@ -1444,22 +1497,21 @@ converse.plugins.add('converse-muc-views', {
this.renderAfterTransition();
},
getAndRenderConfigurationForm () {
/* Start the process of configuring a groupchat, either by
/**
* 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
* Backbone.Model.
*
* Stores the new configuration on the Backbone.Model once
* completed.
*
* Paremeters:
* (Event) ev: DOM event that might be passed in if this
* {@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()
@ -1471,35 +1523,10 @@ converse.plugins.add('converse-muc-views', {
},
hideChatRoomContents () {
return;
},
renderNicknameButton () {
this.el.querySelector('.setNicknameButtonForm').classList.remove('hidden');
this.el.querySelector('.sendXMPPMessage').classList.add('hidden');
this.el.querySelector('.setNicknameButtonForm').addEventListener('submit', this.renderNicknameModal.bind(this), false);
},
renderNicknameModal (ev) {
/* Render a button which allows the user to get a modal to set their nickname.
*/
ev.preventDefault();
const message = this.model.get('nickname_validation_message');
this.model.save('nickname_validation_message', undefined);
if (this.nickname_form_modal === undefined) {
this.nickname_form_modal = new _converse.NicknameFormModal({
//'model': new Backbone.Model({'validation_message': message}),
'model': this.model,
'chatroomview': this,
});
const container_el = this.el.querySelector('.chatroom-body');
container_el.insertAdjacentElement('beforeend', this.nickname_form_modal.el);
} else {
this.nickname_form_modal.model.set('validation_message', message);
if (container_el !== null) {
[].forEach.call(container_el.children, child => child.classList.add('hidden'));
}
this.nickname_form_modal.show(ev);
console.log(this.nickname_form_modal);
u.safeSave(this.model, {'connection_status': converse.ROOMSTATUS.NICKNAME_REQUIRED});
},
renderPasswordForm () {
@ -1598,7 +1625,19 @@ converse.plugins.add('converse-muc-views', {
}
},
insertMessage (view) {
if (_converse.muc_show_logs_before_join &&
this.content.firstElementChild.matches('.empty-history-feedback')) {
this.content.removeChild(this.content.firstElementChild);
}
return _converse.ChatBoxView.prototype.insertMessage.call(this, view);
},
insertNotification (message) {
if (_converse.muc_show_logs_before_join &&
this.content.firstElementChild.matches('.empty-history-feedback')) {
this.content.removeChild(this.content.firstElementChild);
}
this.content.insertAdjacentHTML(
'beforeend',
tpl_info({
@ -1777,13 +1816,16 @@ converse.plugins.add('converse-muc-views', {
this.scrollDown();
},
renderAfterTransition () {
/* Rerender the groupchat after some kind of transition. For
/**
* 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 () {
if (this.model.get('connection_status') == converse.ROOMSTATUS.NICKNAME_REQUIRED) {
this.renderNicknameButton();
this.renderNicknameForm();
} else if (this.model.get('connection_status') == converse.ROOMSTATUS.PASSWORD_REQUIRED) {
this.renderPasswordForm();
} else if (this.model.get('connection_status') == converse.ROOMSTATUS.ENTERED) {
@ -1801,11 +1843,14 @@ converse.plugins.add('converse-muc-views', {
container_el.insertAdjacentHTML('afterbegin', tpl_spinner());
},
hideSpinner () {
/* Check if the spinner is being shown and if so, hide it.
/**
* 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.el.querySelector('.spinner');
if (spinner !== null) {
u.removeElement(spinner);
@ -1845,9 +1890,13 @@ converse.plugins.add('converse-muc-views', {
});
_converse.RoomsPanel = Backbone.NativeView.extend({
/* Backbone.NativeView which renders MUC section of the control box.
/**
* Backbone.NativeView which renders MUC section of the control box.
* @class
* @namespace _converse.RoomsPanel
* @memberOf _converse
*/
_converse.RoomsPanel = Backbone.NativeView.extend({
tagName: 'div',
className: 'controlbox-section',
id: 'chatrooms',
@ -1884,7 +1933,7 @@ converse.plugins.add('converse-muc-views', {
_converse.MUCConfigForm = Backbone.VDOMView.extend({
className: 'muc-config-form',
events: {
'submit form': 'submitConfigForm',
'submit .chatroom-form': 'submitConfigForm',
'click .button-cancel': 'closeConfigForm'
},
@ -1961,55 +2010,6 @@ converse.plugins.add('converse-muc-views', {
});
_converse.NicknameFormModal = _converse.BootstrapModal.extend({
events: {
'submit form': 'submitNickname',
},
initialize (attrs) {
_converse.BootstrapModal.prototype.initialize.apply(this, arguments);
this.chatroomview = attrs.chatroomview;
this.listenTo(this.model, 'change:validation_message', this.render);
this.render();
},
toHTML () {
const err_msg = this.model.get('validation_message');
return tpl_chatroom_nickname_form_modal({
'__': __,
'heading': __('Please choose your nickname'),
'label_nickname': __('Nickname'),
'label_join': __('Enter groupchat'),
'error_class': err_msg ? 'error' : '',
'validation_message': err_msg,
'nickname': this.model.get('nickname')
});
},
submitNickname (ev) {
/* Get the nickname value from the form and then join the
* groupchat with it.
*/
ev.preventDefault();
const nick_el = ev.target.nick;
const nick = nick_el.value.trim();
if (nick) {
this.chatroomview.model.join(nick);
this.model.set({
'validation_message': null,
'nickname': nick
});
} else {
return this.model.set({
'validation_message': __('You need to provide a nickname')
});
}
this.modal.hide();
}
});
_converse.ChatRoomOccupantView = Backbone.VDOMView.extend({
tagName: 'li',
initialize () {

View File

@ -58,6 +58,7 @@ converse.plugins.add('converse-mam', {
/**
* Fetches messages that might have been archived *after*
* the last archived message in our local cache.
* @private
*/
fetchNewestMessages () {
if (this.disable_mam) {
@ -165,8 +166,9 @@ converse.plugins.add('converse-mam', {
Object.assign(_converse.ChatRoom.prototype, {
fetchArchivedMessagesIfNecessary () {
if (this.get('connection_status') !== converse.ROOMSTATUS.ENTERED ||
!this.get('mam_enabled') ||
const conn_status = this.get('connection_status');
if ((!_converse.muc_show_logs_before_join && conn_status !== converse.ROOMSTATUS.ENTERED) ||
!this.features.get('mam_enabled') ||
this.get('mam_initialized')) {
return;
}
@ -177,7 +179,7 @@ converse.plugins.add('converse-mam', {
_converse.onMAMError = function (iq) {
if (iq.querySelectorAll('feature-not-implemented').length) {
if (iq && iq.querySelectorAll('feature-not-implemented').length) {
log.warn("Message Archive Management (XEP-0313) not supported by this server");
} else {
log.error("An error occured while trying to set archiving preferences.");
@ -232,7 +234,11 @@ converse.plugins.add('converse-mam', {
_converse.api.listen.on('addClientFeatures', () => _converse.api.disco.own.features.add(Strophe.NS.MAM));
_converse.api.listen.on('serviceDiscovered', getMAMPrefsFromFeature);
_converse.api.listen.on('enteredNewRoom', chat => chat.fetchNewestMessages());
_converse.api.listen.on('enteredNewRoom', room => room.features.get('mam_enabled') && room.fetchNewestMessages());
_converse.api.listen.on('chatRoomOpened', (view) => {
view.listenTo(view.model, 'change:connection_status', () => view.model.fetchArchivedMessagesIfNecessary());
});
_converse.api.listen.on('chatReconnected', chat => {
// XXX: For MUCs, we listen to enteredNewRoom instead
if (chat.get('type') === _converse.PRIVATE_CHAT_TYPE) {
@ -250,11 +256,6 @@ converse.plugins.add('converse-mam', {
chat.fetchNewestMessages();
}
});
_converse.api.listen.on('chatRoomOpened', (room) => {
room.on('change:mam_enabled', room.fetchArchivedMessagesIfNecessary, room);
room.on('change:connection_status', room.fetchArchivedMessagesIfNecessary, room);
});
/************************ END Event Handlers **************************/

View File

@ -129,7 +129,8 @@ converse.plugins.add('converse-muc', {
'muc_fetch_members': true,
'muc_history_max_stanzas': undefined,
'muc_instant_rooms': true,
'muc_nickname_from_jid': false
'muc_nickname_from_jid': false,
'muc_show_logs_before_join': false
});
_converse.api.promises.add(['roomsAutoJoined']);

View File

@ -1,4 +1,8 @@
<div class="chat-area col">
<div class="chat-content {[ if (o.show_send_button) { ]}chat-content-sendbutton{[ } ]}" aria-live="polite"></div>
<div class="chat-content {[ if (o.show_send_button) { ]}chat-content-sendbutton{[ } ]}" aria-live="polite">
{[ if (o.muc_show_logs_before_join) { ]}
<div class="empty-history-feedback"><span>{{{o.__('No message history available.')}}}</span></div>
{[ } ]}
</div>
<div class="bottom-panel"></div>
</div>

View File

@ -1,6 +1,10 @@
{[ if (o.can_edit) { ]}
<div class="emoji-picker__container dropup"></div>
<div class="message-form-container">
{[ if (o.entered) { ]}
{[ if (o.can_edit) { ]}
<div class="emoji-picker__container dropup"></div>
<div class="message-form-container">
{[ } else { ]}
<div class="muc-bottom-panel">{{{o.__("You're not allowed to send messages in this room")}}}</div>
{[ } ]}
{[ } else { ]}
<div class="muc-bottom-panel">{{{o.__("You're not allowed to send messages in this room")}}}</div>
<div class="muc-bottom-panel"></div>
{[ } ]}

View File

@ -0,0 +1,13 @@
<div class="chatroom-form-container muc-nickname-form">
<form class="converse-form chatroom-form converse-centered-form">
<fieldset class="form-group">
<label>{{{o.heading}}}</label>
<p class="validation-message">{{{o.validation_message}}}</p>
<input type="text" required="required" name="nick" value="{{{o.nickname}}}"
class="form-control {{o.error_class}}" placeholder="{{{o.label_nickname}}}"/>
</fieldset>
<fieldset class="form-group">
<input type="submit" class="btn btn-primary" name="join" value="{{{o.label_join}}}"/>
</fieldset>
</form>
</div>

View File

@ -1,26 +0,0 @@
<div class="modal" id="ask-nickname-modal" tabindex="-1" role="dialog" aria-labelledby="ask-nickname-modal-label" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"
id="ask-nickname-modal-label">{{{o.heading}}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<form class="converse-form chatroom-form converse-centered-form">
<fieldset class="form-group">
<label>{{{o.__('Nickname:')}}}</label>
<p class="validation-message">{{{o.validation_message}}}</p>
<input type="text" required="required" name="nick" value="{{{o.nickname}}}"
class="form-control {{o.error_class}}" placeholder="{{{o.label_nickname}}}"/>
</fieldset>
<fieldset class="form-group">
<input type="submit" class="btn btn-primary" name="join" value="{{{o.label_join}}}"/>
</fieldset>
</form>
</div>
</div>
</div>
</div>

View File

@ -19,7 +19,7 @@
converse.initialize({
auto_away: 300,
auto_register_muc_nickname: true,
debug: true,
loglevel: 'debug',
enable_smacks: true,
i18n: 'en',
message_archiving: 'always',
@ -27,6 +27,7 @@
muc_respect_autojoin: true,
view_mode: 'fullscreen',
websocket_url: 'ws://chat.example.org:5380/xmpp-websocket',
muc_show_logs_before_join: true,
whitelisted_plugins: ['converse-debug'],
});
</script>