Commit aa86a8be authored by JC Brand's avatar JC Brand

muc: Store room configuration (e.g. disco#info `fields`) on the MUC

This will make it easier to add config-based functionality, such as
allowing/showing the `/topic` slash command only to those users who are
allowed to set the subject.
parent 929a00e1
...@@ -102,10 +102,10 @@ ...@@ -102,10 +102,10 @@
['rosterGroupsFetched', 'chatBoxesFetched'], {}, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
async function (done, _converse) { async function (done, _converse) {
// Mock 'getRoomFeatures', otherwise the room won't be // Mock 'getDiscoInfo', otherwise the room won't be
// displayed as it waits first for the features to be returned // displayed as it waits first for the features to be returned
// (when it's a new room being created). // (when it's a new room being created).
spyOn(_converse.ChatRoom.prototype, 'getRoomFeatures').and.callFake(() => Promise.resolve()); spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
let jid = 'lounge@montague.lit'; let jid = 'lounge@montague.lit';
let chatroomview, IQ_id; let chatroomview, IQ_id;
...@@ -3009,13 +3009,13 @@ ...@@ -3009,13 +3009,13 @@
textarea.value = '/help'; textarea.value = '/help';
view.onKeyDown(enter); view.onKeyDown(enter);
info_messages = sizzle('.chat-info:not(.chat-event)', view.el); info_messages = sizzle('.chat-info:not(.chat-event)', view.el);
expect(info_messages.length).toBe(19); expect(info_messages.length).toBe(17);
let commands = info_messages.map(m => m.textContent.replace(/:.*$/, '')); let commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
expect(commands).toEqual([ expect(commands).toEqual([
"You can run the following commands", "You can run the following commands",
"/admin", "/ban", "/clear", "/deop", "/destroy", "/admin", "/ban", "/clear", "/deop", "/destroy",
"/help", "/kick", "/me", "/member", "/modtools", "/mute", "/nick", "/help", "/kick", "/me", "/member", "/modtools", "/mute", "/nick",
"/op", "/register", "/revoke", "/subject", "/topic", "/voice" "/op", "/register", "/revoke", "/voice"
]); ]);
occupant.set('affiliation', 'member'); occupant.set('affiliation', 'member');
textarea.value = '/clear'; textarea.value = '/clear';
...@@ -3025,9 +3025,9 @@ ...@@ -3025,9 +3025,9 @@
textarea.value = '/help'; textarea.value = '/help';
view.onKeyDown(enter); view.onKeyDown(enter);
info_messages = sizzle('.chat-info', view.el).slice(1); info_messages = sizzle('.chat-info', view.el).slice(1);
expect(info_messages.length).toBe(11); expect(info_messages.length).toBe(9);
commands = info_messages.map(m => m.textContent.replace(/:.*$/, '')); commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
expect(commands).toEqual(["/clear", "/help", "/kick", "/me", "/modtools", "/mute", "/nick", "/register", "/subject", "/topic", "/voice"]); expect(commands).toEqual(["/clear", "/help", "/kick", "/me", "/modtools", "/mute", "/nick", "/register", "/voice"]);
occupant.set('role', 'participant'); occupant.set('role', 'participant');
textarea = view.el.querySelector('.chat-textarea'); textarea = view.el.querySelector('.chat-textarea');
...@@ -3035,6 +3035,20 @@ ...@@ -3035,6 +3035,20 @@
view.onKeyDown(enter); view.onKeyDown(enter);
await u.waitUntil(() => sizzle('.chat-info:not(.chat-event)', view.el).length === 0); await u.waitUntil(() => sizzle('.chat-info:not(.chat-event)', view.el).length === 0);
textarea.value = '/help';
view.onKeyDown(enter);
info_messages = sizzle('.chat-info', view.el).slice(1);
expect(info_messages.length).toBe(5);
commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
expect(commands).toEqual(["/clear", "/help", "/me", "/nick", "/register"]);
// Test that /topic is available if all users may change the subject
// Note: we're making a shortcut here, this value should never be set manually
view.model.config.set('changesubject', true);
textarea.value = '/clear';
view.onKeyDown(enter);
await u.waitUntil(() => sizzle('.chat-info:not(.chat-event)', view.el).length === 0);
textarea.value = '/help'; textarea.value = '/help';
view.onKeyDown(enter); view.onKeyDown(enter);
info_messages = sizzle('.chat-info', view.el).slice(1); info_messages = sizzle('.chat-info', view.el).slice(1);
...@@ -4572,7 +4586,7 @@ ...@@ -4572,7 +4586,7 @@
nick_input.value = 'romeo'; nick_input.value = 'romeo';
expect(modal.el.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat'); expect(modal.el.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat');
spyOn(_converse.ChatRoom.prototype, 'getRoomFeatures').and.callFake(() => Promise.resolve()); spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
modal.el.querySelector('input[name="chatroom"]').value = 'lounce@muc.montague.lit'; modal.el.querySelector('input[name="chatroom"]').value = 'lounce@muc.montague.lit';
modal.el.querySelector('form input[type="submit"]').click(); modal.el.querySelector('form input[type="submit"]').click();
...@@ -4660,7 +4674,7 @@ ...@@ -4660,7 +4674,7 @@
const modal = roomspanel.add_room_modal; const modal = roomspanel.add_room_modal;
await u.waitUntil(() => u.isVisible(modal.el), 1000) await u.waitUntil(() => u.isVisible(modal.el), 1000)
expect(modal.el.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat'); expect(modal.el.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat');
spyOn(_converse.ChatRoom.prototype, 'getRoomFeatures').and.callFake(() => Promise.resolve()); spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
const label_name = modal.el.querySelector('label[for="chatroom"]'); const label_name = modal.el.querySelector('label[for="chatroom"]');
expect(label_name.textContent.trim()).toBe('Groupchat name:'); expect(label_name.textContent.trim()).toBe('Groupchat name:');
...@@ -4700,7 +4714,7 @@ ...@@ -4700,7 +4714,7 @@
const modal = roomspanel.add_room_modal; const modal = roomspanel.add_room_modal;
await u.waitUntil(() => u.isVisible(modal.el), 1000) await u.waitUntil(() => u.isVisible(modal.el), 1000)
expect(modal.el.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat'); expect(modal.el.querySelector('.modal-title').textContent.trim()).toBe('Enter a new Groupchat');
spyOn(_converse.ChatRoom.prototype, 'getRoomFeatures').and.callFake(() => Promise.resolve()); spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
const label_name = modal.el.querySelector('label[for="chatroom"]'); const label_name = modal.el.querySelector('label[for="chatroom"]');
expect(label_name.textContent.trim()).toBe('Groupchat name:'); expect(label_name.textContent.trim()).toBe('Groupchat name:');
...@@ -4742,7 +4756,7 @@ ...@@ -4742,7 +4756,7 @@
test_utils.closeControlBox(_converse); test_utils.closeControlBox(_converse);
const modal = roomspanel.list_rooms_modal; const modal = roomspanel.list_rooms_modal;
await u.waitUntil(() => u.isVisible(modal.el), 1000); await u.waitUntil(() => u.isVisible(modal.el), 1000);
spyOn(_converse.ChatRoom.prototype, 'getRoomFeatures').and.callFake(() => Promise.resolve()); spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
// See: https://xmpp.org/extensions/xep-0045.html#disco-rooms // See: https://xmpp.org/extensions/xep-0045.html#disco-rooms
...@@ -4836,7 +4850,7 @@ ...@@ -4836,7 +4850,7 @@
test_utils.closeControlBox(_converse); test_utils.closeControlBox(_converse);
const modal = roomspanel.list_rooms_modal; const modal = roomspanel.list_rooms_modal;
await u.waitUntil(() => u.isVisible(modal.el), 1000); await u.waitUntil(() => u.isVisible(modal.el), 1000);
spyOn(_converse.ChatRoom.prototype, 'getRoomFeatures').and.callFake(() => Promise.resolve()); spyOn(_converse.ChatRoom.prototype, 'getDiscoInfo').and.callFake(() => Promise.resolve());
roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called roomspanel.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
expect(modal.el.querySelector('input[name="server"]')).toBe(null); expect(modal.el.querySelector('input[name="server"]')).toBe(null);
......
...@@ -625,6 +625,7 @@ converse.plugins.add('converse-muc-views', { ...@@ -625,6 +625,7 @@ converse.plugins.add('converse-muc-views', {
this.model.toJSON(), { this.model.toJSON(), {
'_': _, '_': _,
'__': __, '__': __,
'config': this.model.config.toJSON(),
'display_name': __('Groupchat info for %1$s', this.model.getDisplayName()), 'display_name': __('Groupchat info for %1$s', this.model.getDisplayName()),
'features': this.model.features.toJSON(), 'features': this.model.features.toJSON(),
'num_occupants': this.model.occupants.length, 'num_occupants': this.model.occupants.length,
...@@ -1126,7 +1127,7 @@ converse.plugins.add('converse-muc-views', { ...@@ -1126,7 +1127,7 @@ converse.plugins.add('converse-muc-views', {
'info_close': __('Close and leave this groupchat'), 'info_close': __('Close and leave this groupchat'),
'info_configure': __('Configure this groupchat'), 'info_configure': __('Configure this groupchat'),
'info_details': __('Show more details about this groupchat'), 'info_details': __('Show more details about this groupchat'),
'description': u.addHyperlinks(xss.filterXSS(_.get(this.model.get('subject'), 'text'), {'whiteList': {}})), 'subject': u.addHyperlinks(xss.filterXSS(_.get(this.model.get('subject'), 'text'), {'whiteList': {}})),
})); }));
}, },
...@@ -1365,7 +1366,10 @@ converse.plugins.add('converse-muc-views', { ...@@ -1365,7 +1366,10 @@ converse.plugins.add('converse-muc-views', {
onCommandError (err) { onCommandError (err) {
log.fatal(err); log.fatal(err);
this.showErrorMessage(__("Sorry, an error happened while running the command. Check your browser's developer console for details.")); this.showErrorMessage(
__("Sorry, an error happened while running the command.") + " " +
__("Check your browser's developer console for details.")
);
}, },
getAllowedCommands () { getAllowedCommands () {
...@@ -2063,9 +2067,20 @@ converse.plugins.add('converse-muc-views', { ...@@ -2063,9 +2067,20 @@ converse.plugins.add('converse-muc-views', {
}); });
}, },
submitConfigForm (ev) { async submitConfigForm (ev) {
ev.preventDefault(); ev.preventDefault();
this.model.saveConfiguration(ev.target).then(() => this.model.refreshRoomFeatures()); const inputs = sizzle(':input:not([type=button]):not([type=submit])', ev.target);
const configArray = inputs.map(u.webForm2xForm);
try {
await this.model.sendConfiguration(configArray);
} catch (e) {
log.error(e);
this.showErrorMessage(
__("Sorry, an error occurred while trying to submit the config form.") + " " +
__("Check your browser's developer console for details.")
);
}
await this.model.refreshDiscoInfo();
this.chatroomview.closeForm(); this.chatroomview.closeForm();
}, },
......
...@@ -692,18 +692,17 @@ converse.plugins.add('converse-disco', { ...@@ -692,18 +692,17 @@ converse.plugins.add('converse-disco', {
}, },
/** /**
* Refresh the features (and fields and identities) associated with a * Refresh the features, fields and identities associated with a
* disco entity by refetching them from the server * disco entity by refetching them from the server
* * @method _converse.api.disco.refresh
* @method _converse.api.disco.refreshFeatures
* @param {string} jid The JID of the entity whose features are refreshed. * @param {string} jid The JID of the entity whose features are refreshed.
* @returns {promise} A promise which resolves once the features have been refreshed * @returns {promise} A promise which resolves once the features have been refreshed
* @example * @example
* await _converse.api.disco.refreshFeatures('room@conference.example.org'); * await _converse.api.disco.refresh('room@conference.example.org');
*/ */
async refreshFeatures (jid) { async refresh (jid) {
if (!jid) { if (!jid) {
throw new TypeError('api.disco.refreshFeatures: You need to provide an entity JID'); throw new TypeError('api.disco.refresh: You need to provide an entity JID');
} }
await _converse.api.waitUntil('discoInitialized'); await _converse.api.waitUntil('discoInitialized');
let entity = await _converse.api.disco.entities.get(jid); let entity = await _converse.api.disco.entities.get(jid);
...@@ -722,6 +721,14 @@ converse.plugins.add('converse-disco', { ...@@ -722,6 +721,14 @@ converse.plugins.add('converse-disco', {
return entity.waitUntilFeaturesDiscovered; return entity.waitUntilFeaturesDiscovered;
}, },
/**
* @deprecated Use {@link _converse.api.disco.refresh} instead.
* @method _converse.api.disco.refreshFeatures
*/
refreshFeatures (jid) {
return _converse.api.refresh(jid);
},
/** /**
* Return all the features associated with a disco entity * Return all the features associated with a disco entity
* *
......
This diff is collapsed.
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
<div class="room-info"> <div class="room-info">
<p class="room-info"><strong>{{{o.__('Name')}}}</strong>: {{{o.name}}}</p> <p class="room-info"><strong>{{{o.__('Name')}}}</strong>: {{{o.name}}}</p>
<p class="room-info"><strong>{{{o.__('Groupchat address (JID)')}}}</strong>: {{{o.jid}}}</p> <p class="room-info"><strong>{{{o.__('Groupchat address (JID)')}}}</strong>: {{{o.jid}}}</p>
<p class="room-info"><strong>{{{o.__('Description')}}}</strong>: {{{o.description}}}</p> <p class="room-info"><strong>{{{o.__('Description')}}}</strong>: {{{o.config.description}}}</p>
{[ if (o.subject) { ]} {[ if (o.subject) { ]}
<p class="room-info"><strong>{{{o.__('Topic')}}}</strong>: {{o.topic}}</p> <!-- Sanitized in converse-muc-views. We want to render links. --> <p class="room-info"><strong>{{{o.__('Topic')}}}</strong>: {{o.topic}}</p> <!-- Sanitized in converse-muc-views. We want to render links. -->
<p class="room-info"><strong>{{{o.__('Topic author')}}}</strong>: {{{o._.get(o.subject, 'author')}}}</p> <p class="room-info"><strong>{{{o.__('Topic author')}}}</strong>: {{{o._.get(o.subject, 'author')}}}</p>
......
...@@ -14,4 +14,4 @@ ...@@ -14,4 +14,4 @@
</div> </div>
</div> </div>
<!-- Sanitized in converse-muc-views. We want to render links. --> <!-- Sanitized in converse-muc-views. We want to render links. -->
<p class="chat-head__desc">{{o.description}}</p> <p class="chat-head__desc">{{o.subject}}</p>
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
persistent_store: 'IndexedDB', persistent_store: 'IndexedDB',
muc_domain: 'conference.chat.example.org', muc_domain: 'conference.chat.example.org',
muc_respect_autojoin: true, muc_respect_autojoin: true,
view_mode: 'overlayed', view_mode: 'fullscreen',
websocket_url: 'ws://chat.example.org:5380/xmpp-websocket', websocket_url: 'ws://chat.example.org:5380/xmpp-websocket',
// bosh_service_url: 'http://chat.example.org:5280/http-bind', // bosh_service_url: 'http://chat.example.org:5280/http-bind',
muc_show_logs_before_join: true, muc_show_logs_before_join: true,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment