diff --git a/spec/messages.js b/spec/messages.js index 2c83e8632..46d53777c 100644 --- a/spec/messages.js +++ b/spec/messages.js @@ -2344,7 +2344,8 @@ .c('status').attrs({code:'210'}).nodeTree; _converse.connection._dataRecv(test_utils.createRequest(presence)); view.model.sendMessage('hello world'); - await new Promise((resolve, reject) => view.once('messageInserted', resolve)); + await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-msg').length === 3); + expect(view.model.messages.last().get('affiliation')).toBe('owner'); expect(view.model.messages.last().get('role')).toBe('moderator'); expect(view.el.querySelectorAll('.chat-msg').length).toBe(3); diff --git a/spec/muc.js b/spec/muc.js index 06c70a207..c63b1af80 100644 --- a/spec/muc.js +++ b/spec/muc.js @@ -359,7 +359,7 @@ */ const presence = $pres({ to:'romeo@montague.lit/orchard', - from:'lounge@montague.lit/thirdwitch', + from:'lounge@montague.lit/nicky', id:'5025e055-036c-4bc5-a227-706e7e352053' }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) .c('item').attrs({ @@ -371,9 +371,12 @@ .c('status').attrs({code:'201'}).nodeTree; _converse.connection._dataRecv(test_utils.createRequest(presence)); - const info_text = view.el.querySelector('.chat-content .chat-info').textContent; - expect(info_text).toBe('A new groupchat has been created'); + await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length === 2); + + const info_texts = Array.from(view.el.querySelectorAll('.chat-content .chat-info')).map(e => e.textContent); + expect(info_texts[0]).toBe('A new groupchat has been created'); + expect(info_texts[1]).toBe('nicky has entered the groupchat'); // An instant room is created by saving the default configuratoin. // @@ -448,7 +451,7 @@ done() })); - it("shows a notification if its not anonymous", + it("shows a notification if it's not anonymous", mock.initConverse( null, ['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) { @@ -465,27 +468,7 @@ * * */ - let presence = $pres({ - to: 'romeo@montague.lit/orchard', - from: 'coven@chat.shakespeare.lit/some1' - }).c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'owner', - 'jid': 'romeo@montague.lit/_converse.js-29092160', - 'role': 'moderator' - }).up() - .c('status', {code: '110'}).up() - .c('status', {code: '100'}); - - _converse.connection._dataRecv(test_utils.createRequest(presence)); - expect(chat_content.querySelectorAll('.chat-info').length).toBe(2); - expect(sizzle('div.chat-info:first', chat_content).pop().textContent) - .toBe("This groupchat is not anonymous"); - expect(sizzle('div.chat-info:last', chat_content).pop().textContent) - .toBe("some1 has entered the groupchat"); - - // Check that we don't show the notification twice - presence = $pres({ + const presence = $pres({ to: 'romeo@montague.lit/orchard', from: 'coven@chat.shakespeare.lit/some1' }).c('x', {xmlns: Strophe.NS.MUC_USER}) @@ -497,7 +480,8 @@ .c('status', {code: '110'}).up() .c('status', {code: '100'}); _converse.connection._dataRecv(test_utils.createRequest(presence)); - expect(chat_content.querySelectorAll('.chat-info').length).toBe(2); + + await test_utils.waitUntil(() => chat_content.querySelectorAll('.chat-info').length === 2); expect(sizzle('div.chat-info:first', chat_content).pop().textContent) .toBe("This groupchat is not anonymous"); expect(sizzle('div.chat-info:last', chat_content).pop().textContent) @@ -1814,6 +1798,7 @@ .c('status').attrs({code:'210'}).nodeTree; _converse.connection._dataRecv(test_utils.createRequest(presence)); + await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length === 2); const info_text = sizzle('.chat-content .chat-info:first', view.el).pop().textContent; expect(info_text).toBe('Your nickname has been automatically set to thirdwitch'); done(); @@ -2096,7 +2081,7 @@ done(); })); - it("informs users if their nicknames has been changed.", + it("informs users if their nicknames have been changed.", mock.initConverse( null, ['rosterGroupsFetched'], {}, async function (done, _converse) { @@ -2167,11 +2152,12 @@ .c('status').attrs({code:'110'}).nodeTree; _converse.connection._dataRecv(test_utils.createRequest(presence)); - expect(chat_content.querySelectorAll('div.chat-info').length).toBe(2); + await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 2); + expect(sizzle('div.chat-info:last').pop().textContent).toBe( __(_converse.muc.new_nickname_messages["303"], "newnick") ); - expect(view.model.get('connection_status')).toBe(converse.ROOMSTATUS.DISCONNECTED); + expect(view.model.get('connection_status')).toBe(converse.ROOMSTATUS.ENTERED); occupants = view.el.querySelector('.occupant-list'); expect(occupants.childNodes.length).toBe(1); @@ -2509,6 +2495,7 @@ .c('status', {code: '104'}).up() .c('status', {code: '172'}); _converse.connection._dataRecv(test_utils.createRequest(message)); + await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-content .chat-info').length === 3); const chat_body = view.el.querySelector('.chatroom-body'); expect(sizzle('.message:last', chat_body).pop().textContent) .toBe('This groupchat is now no longer anonymous'); @@ -2551,6 +2538,7 @@ .up() .c('status').attrs({code:'110'}).up() .c('status').attrs({code:'307'}).nodeTree; + _converse.connection._dataRecv(test_utils.createRequest(presence)); const view = _converse.chatboxviews.get('lounge@montague.lit'); @@ -3232,6 +3220,8 @@ }).up() .c('status', {'code': '307'}); _converse.connection._dataRecv(test_utils.createRequest(presence)); + + await test_utils.waitUntil(() => view.el.querySelectorAll('.chat-info').length === 4); expect(view.el.querySelectorAll('.chat-info')[3].textContent).toBe("annoying guy has been kicked out"); expect(view.el.querySelectorAll('.chat-info').length).toBe(4); done(); @@ -3628,6 +3618,33 @@ const groupchat_jid = 'members-only@muc.montague.lit' await test_utils.openChatRoomViaModal(_converse, groupchat_jid, 'romeo'); + const view = _converse.chatboxviews.get(groupchat_jid); + const iq = await test_utils.waitUntil(() => _.filter( + _converse.connection.IQ_stanzas, + iq => iq.querySelector( + `iq[to="${groupchat_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` + )).pop()); + + // State that the chat is members-only via the features IQ + const features_stanza = $iq({ + 'from': groupchat_jid, + 'id': iq.getAttribute('id'), + 'to': 'romeo@montague.lit/desktop', + 'type': 'result' + }) + .c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) + .c('identity', { + 'category': 'conference', + 'name': 'A Dark Cave', + 'type': 'text' + }).up() + .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() + .c('feature', {'var': 'muc_hidden'}).up() + .c('feature', {'var': 'muc_temporary'}).up() + .c('feature', {'var': 'muc_membersonly'}).up(); + _converse.connection._dataRecv(test_utils.createRequest(features_stanza)); + await test_utils.waitUntil(() => view.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING); + const presence = $pres().attrs({ from: `${groupchat_jid}/romeo`, id: u.getUniqueId(), @@ -3637,8 +3654,6 @@ .c('error').attrs({by:'lounge@montague.lit', type:'auth'}) .c('registration-required').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; - const view = _converse.chatboxviews.get(groupchat_jid); - spyOn(view, 'showErrorMessage').and.callThrough(); _converse.connection._dataRecv(test_utils.createRequest(presence)); expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent) .toBe('You are not on the member list of this groupchat.'); @@ -3652,6 +3667,29 @@ const groupchat_jid = 'off-limits@muc.montague.lit' await test_utils.openChatRoomViaModal(_converse, groupchat_jid, 'romeo'); + + const iq = await test_utils.waitUntil(() => _.filter( + _converse.connection.IQ_stanzas, + iq => iq.querySelector( + `iq[to="${groupchat_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` + )).pop()); + + const features_stanza = $iq({ + 'from': groupchat_jid, + 'id': iq.getAttribute('id'), + 'to': 'romeo@montague.lit/desktop', + 'type': 'result' + }) + .c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) + .c('identity', {'category': 'conference', 'name': 'A Dark Cave', 'type': 'text'}).up() + .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() + .c('feature', {'var': 'muc_hidden'}).up() + .c('feature', {'var': 'muc_temporary'}).up() + _converse.connection._dataRecv(test_utils.createRequest(features_stanza)); + + const view = _converse.chatboxviews.get(groupchat_jid); + await test_utils.waitUntil(() => view.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING); + const presence = $pres().attrs({ from: `${groupchat_jid}/romeo`, id: u.getUniqueId(), @@ -3661,7 +3699,6 @@ .c('error').attrs({by:'lounge@montague.lit', type:'auth'}) .c('forbidden').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; - const view = _converse.chatboxviews.get(groupchat_jid); spyOn(view, 'showErrorMessage').and.callThrough(); _converse.connection._dataRecv(test_utils.createRequest(presence)); expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent) @@ -3697,6 +3734,7 @@ done(); })); + it("will automatically choose a new nickname if a nickname conflict happens and muc_nickname_from_jid=true", mock.initConverse( null, ['rosterGroupsFetched'], {}, @@ -3765,7 +3803,26 @@ const groupchat_jid = 'impermissable@muc.montague.lit' await test_utils.openChatRoomViaModal(_converse, groupchat_jid, 'romeo') - var presence = $pres().attrs({ + + // We pretend this is a new room, so no disco info is returned. + const iq = await test_utils.waitUntil(() => _.filter( + _converse.connection.IQ_stanzas, + iq => iq.querySelector( + `iq[to="${groupchat_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` + )).pop()); + const features_stanza = $iq({ + 'from': 'room@conference.example.org', + 'id': iq.getAttribute('id'), + 'to': 'romeo@montague.lit/desktop', + 'type': 'error' + }).c('error', {'type': 'cancel'}) + .c('item-not-found', {'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas"}); + _converse.connection._dataRecv(test_utils.createRequest(features_stanza)); + + const view = _converse.chatboxviews.get(groupchat_jid); + await test_utils.waitUntil(() => (view.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING)); + + const presence = $pres().attrs({ from: `${groupchat_jid}/romeo`, id: u.getUniqueId(), to:'romeo@montague.lit/pda', @@ -3773,7 +3830,6 @@ }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() .c('error').attrs({by:'lounge@montague.lit', type:'cancel'}) .c('not-allowed').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; - const view = _converse.chatboxviews.get(groupchat_jid); spyOn(view, 'showErrorMessage').and.callThrough(); _converse.connection._dataRecv(test_utils.createRequest(presence)); expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent) @@ -3788,6 +3844,25 @@ const groupchat_jid = 'conformist@muc.montague.lit' await test_utils.openChatRoomViaModal(_converse, groupchat_jid, 'romeo'); + + const iq = await test_utils.waitUntil(() => _.filter( + _converse.connection.IQ_stanzas, + iq => iq.querySelector( + `iq[to="${groupchat_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` + )).pop()); + const features_stanza = $iq({ + 'from': groupchat_jid, + 'id': iq.getAttribute('id'), + 'to': 'romeo@montague.lit/desktop', + 'type': 'result' + }).c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) + .c('identity', {'category': 'conference', 'name': 'A Dark Cave', 'type': 'text'}).up() + .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() + _converse.connection._dataRecv(test_utils.createRequest(features_stanza)); + + const view = _converse.chatboxviews.get(groupchat_jid); + await test_utils.waitUntil(() => (view.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING)); + const presence = $pres().attrs({ from: `${groupchat_jid}/romeo`, id: u.getUniqueId(), @@ -3797,7 +3872,6 @@ .c('error').attrs({by:'lounge@montague.lit', type:'cancel'}) .c('not-acceptable').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; - const view = _converse.chatboxviews.get(groupchat_jid); spyOn(view, 'showErrorMessage').and.callThrough(); _converse.connection._dataRecv(test_utils.createRequest(presence)); expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent) @@ -3812,6 +3886,25 @@ const groupchat_jid = 'nonexistent@muc.montague.lit' await test_utils.openChatRoomViaModal(_converse, groupchat_jid, 'romeo'); + + const iq = await test_utils.waitUntil(() => _.filter( + _converse.connection.IQ_stanzas, + iq => iq.querySelector( + `iq[to="${groupchat_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` + )).pop()); + const features_stanza = $iq({ + 'from': groupchat_jid, + 'id': iq.getAttribute('id'), + 'to': 'romeo@montague.lit/desktop', + 'type': 'result' + }).c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) + .c('identity', {'category': 'conference', 'name': 'A Dark Cave', 'type': 'text'}).up() + .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() + _converse.connection._dataRecv(test_utils.createRequest(features_stanza)); + + const view = _converse.chatboxviews.get(groupchat_jid); + await test_utils.waitUntil(() => (view.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING)); + const presence = $pres().attrs({ from: `${groupchat_jid}/romeo`, id: u.getUniqueId(), @@ -3821,7 +3914,6 @@ .c('error').attrs({by:'lounge@montague.lit', type:'cancel'}) .c('item-not-found').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; - const view = _converse.chatboxviews.get(groupchat_jid); spyOn(view, 'showErrorMessage').and.callThrough(); _converse.connection._dataRecv(test_utils.createRequest(presence)); expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent) @@ -3836,6 +3928,25 @@ const groupchat_jid = 'maxed-out@muc.montague.lit' await test_utils.openChatRoomViaModal(_converse, groupchat_jid, 'romeo') + + const iq = await test_utils.waitUntil(() => _.filter( + _converse.connection.IQ_stanzas, + iq => iq.querySelector( + `iq[to="${groupchat_jid}"] query[xmlns="http://jabber.org/protocol/disco#info"]` + )).pop()); + const features_stanza = $iq({ + 'from': groupchat_jid, + 'id': iq.getAttribute('id'), + 'to': 'romeo@montague.lit/desktop', + 'type': 'result' + }).c('query', { 'xmlns': 'http://jabber.org/protocol/disco#info'}) + .c('identity', {'category': 'conference', 'name': 'A Dark Cave', 'type': 'text'}).up() + .c('feature', {'var': 'http://jabber.org/protocol/muc'}).up() + _converse.connection._dataRecv(test_utils.createRequest(features_stanza)); + + const view = _converse.chatboxviews.get(groupchat_jid); + await test_utils.waitUntil(() => (view.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING)); + const presence = $pres().attrs({ from: `${groupchat_jid}/romeo`, id: u.getUniqueId(), @@ -3845,7 +3956,6 @@ .c('error').attrs({by:'lounge@montague.lit', type:'cancel'}) .c('service-unavailable').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; - const view = _converse.chatboxviews.get(groupchat_jid); spyOn(view, 'showErrorMessage').and.callThrough(); _converse.connection._dataRecv(test_utils.createRequest(presence)); expect(view.el.querySelector('.chatroom-body .disconnect-container .disconnect-msg:last-child').textContent) @@ -4878,3 +4988,5 @@ }); }); })); + + diff --git a/src/converse-message-view.js b/src/converse-message-view.js index c0f0174bc..13e32076c 100644 --- a/src/converse-message-view.js +++ b/src/converse-message-view.js @@ -112,6 +112,8 @@ converse.plugins.add('converse-message-view', { this.renderFileUploadProgresBar(); } else if (this.model.get('type') === 'error') { this.renderErrorMessage(); + } else if (this.model.get('type') === 'info') { + this.renderInfoMessage(); } else { await this.renderChatMessage(); } @@ -212,6 +214,16 @@ converse.plugins.add('converse-message-view', { } }, + renderInfoMessage () { + const msg = u.stringToElement( + tpl_info(Object.assign(this.model.toJSON(), { + 'extra_classes': 'chat-info', + 'isodate': dayjs(this.model.get('time')).toISOString() + })) + ); + return this.replaceElement(msg); + }, + renderErrorMessage () { const msg = u.stringToElement( tpl_info(Object.assign(this.model.toJSON(), { diff --git a/src/converse-minimize.js b/src/converse-minimize.js index e44661d3d..280c51e11 100644 --- a/src/converse-minimize.js +++ b/src/converse-minimize.js @@ -197,6 +197,12 @@ converse.plugins.add('converse-minimize', { const minimizableChatBoxView = { + + /** + * Maximizes a minimized chat box. + * Will trigger {@link _converse#chatBoxMaximized} + * @returns {_converse.ChatBoxView|_converse.ChatRoomView} + */ maximize () { // Restores a minimized chat box const { _converse } = this.__super__; @@ -216,6 +222,11 @@ converse.plugins.add('converse-minimize', { return this; }, + /** + * Minimizes a chat box. + * Will trigger {@link _converse#chatBoxMinimized} + * @returns {_converse.ChatBoxView|_converse.ChatRoomView} + */ minimize (ev) { const { _converse } = this.__super__; if (ev && ev.preventDefault) { ev.preventDefault(); } @@ -234,6 +245,7 @@ converse.plugins.add('converse-minimize', { * @example _converse.api.listen.on('chatBoxMinimized', view => { ... }); */ _converse.api.trigger('chatBoxMinimized', this); + return this; }, onMinimizedChanged (item) { diff --git a/src/converse-muc-views.js b/src/converse-muc-views.js index 4b4e7a8bb..57dee8255 100644 --- a/src/converse-muc-views.js +++ b/src/converse-muc-views.js @@ -139,87 +139,6 @@ converse.plugins.add('converse-muc-views', { Object.assign(_converse.ControlBoxView.prototype, { renderRoomsPanel }); } - - function ___ (str) { - /* This is part of a hack to get gettext to scan strings to be - * translated. Strings we cannot send to the function above because - * they require variable interpolation and we don't yet have the - * variables at scan time. - * - * See actionInfoMessages further below. - */ - return str; - } - - /* https://xmpp.org/extensions/xep-0045.html - * ---------------------------------------- - * 100 message Entering a groupchat Inform user that any occupant is allowed to see the user's full JID - * 101 message (out of band) Affiliation change Inform user that his or her affiliation changed while not in the groupchat - * 102 message Configuration change Inform occupants that groupchat now shows unavailable members - * 103 message Configuration change Inform occupants that groupchat now does not show unavailable members - * 104 message Configuration change Inform occupants that a non-privacy-related groupchat configuration change has occurred - * 110 presence Any groupchat presence Inform user that presence refers to one of its own groupchat occupants - * 170 message or initial presence Configuration change Inform occupants that groupchat logging is now enabled - * 171 message Configuration change Inform occupants that groupchat logging is now disabled - * 172 message Configuration change Inform occupants that the groupchat is now non-anonymous - * 173 message Configuration change Inform occupants that the groupchat is now semi-anonymous - * 174 message Configuration change Inform occupants that the groupchat is now fully-anonymous - * 201 presence Entering a groupchat Inform user that a new groupchat has been created - * 210 presence Entering a groupchat Inform user that the service has assigned or modified the occupant's roomnick - * 301 presence Removal from groupchat Inform user that he or she has been banned from the groupchat - * 303 presence Exiting a groupchat Inform all occupants of new groupchat nickname - * 307 presence Removal from groupchat Inform user that he or she has been kicked from the groupchat - * 321 presence Removal from groupchat Inform user that he or she is being removed from the groupchat because of an affiliation change - * 322 presence Removal from groupchat Inform user that he or she is being removed from the groupchat because the groupchat has been changed to members-only and the user is not a member - * 332 presence Removal from groupchat Inform user that he or she is being removed from the groupchat because of a system shutdown - */ - _converse.muc = { - info_messages: { - 100: __('This groupchat is not anonymous'), - 102: __('This groupchat now shows unavailable members'), - 103: __('This groupchat does not show unavailable members'), - 104: __('The groupchat configuration has changed'), - 170: __('groupchat logging is now enabled'), - 171: __('groupchat logging is now disabled'), - 172: __('This groupchat is now no longer anonymous'), - 173: __('This groupchat is now semi-anonymous'), - 174: __('This groupchat is now fully-anonymous'), - 201: __('A new groupchat has been created') - }, - - disconnect_messages: { - 301: __('You have been banned from this groupchat'), - 307: __('You have been kicked from this groupchat'), - 321: __("You have been removed from this groupchat because of an affiliation change"), - 322: __("You have been removed from this groupchat because the groupchat has changed to members-only and you're not a member"), - 332: __("You have been removed from this groupchat because the service hosting it is being shut down") - }, - - action_info_messages: { - /* XXX: Note the triple underscore function and not double - * underscore. - * - * This is a hack. We can't pass the strings to __ because we - * don't yet know what the variable to interpolate is. - * - * Triple underscore will just return the string again, but we - * can then at least tell gettext to scan for it so that these - * strings are picked up by the translation machinery. - */ - 301: ___("%1$s has been banned"), - 303: ___("%1$s's nickname has changed"), - 307: ___("%1$s has been kicked out"), - 321: ___("%1$s has been removed because of an affiliation change"), - 322: ___("%1$s has been removed for not being a member") - }, - - new_nickname_messages: { - 210: ___('Your nickname has been automatically set to %1$s'), - 303: ___('Your nickname has been changed to %1$s') - } - }; - - /* Insert groupchat info (based on returned #disco IQ stanza) * @function insertRoomInfo * @param { HTMLElement } el - The HTML DOM element that contains the info. @@ -581,7 +500,6 @@ converse.plugins.add('converse-muc-views', { this.render(); this.updateAfterMessagesFetched(); this.createOccupantsView(); - this.registerHandlers(); this.onConnectionStatusChanged(); /** * Triggered once a groupchat has been opened @@ -779,6 +697,8 @@ converse.plugins.add('converse-muc-views', { const conn_status = this.model.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) { @@ -786,6 +706,10 @@ converse.plugins.add('converse-muc-views', { this.setChatState(_converse.ACTIVE); this.scrollDown(); this.focus(); + } else if (conn_status === converse.ROOMSTATUS.DISCONNECTED) { + this.showDisconnectMessage(); + } else if (conn_status === converse.ROOMSTATUS.DESTROYED) { + this.showDestroyedMessage(); } }, @@ -1106,7 +1030,8 @@ converse.plugins.add('converse-muc-views', { if (!this.verifyRoles(['visitor', 'participant', 'moderator'])) { break; } else if (args.length === 0) { - this.showErrorMessage(__('You need to provide a nickname')) + // e.g. Your nickname is "coolguy69" + this.showErrorMessage(__('Your nickname is "%1$s"', this.model.get('nick'))) } else { const jid = Strophe.getBareJidFromJid(this.model.get('jid')); _converse.api.send($pres({ @@ -1152,42 +1077,6 @@ converse.plugins.add('converse-muc-views', { return true; }, - registerHandlers () { - /* Register presence and message handlers for this chat - * groupchat - */ - // XXX: Ideally this can be refactored out so that we don't - // need to do stanza processing inside the views in this - // module. See the comment in "onPresence" for more info. - this.model.addHandler('presence', 'ChatRoomView.onPresence', this.onPresence.bind(this)); - // XXX instead of having a method showStatusMessages, we could instead - // create message models in converse-muc.js and then give them views in this module. - this.model.addHandler('message', 'ChatRoomView.showStatusMessages', this.showStatusMessages.bind(this)); - }, - - /** - * Handles all MUC presence stanzas. - * @private - * @method _converse.ChatRoomView#onPresence - * @param { XMLElement } pres - The stanza - */ - onPresence (pres) { - // XXX: Current thinking is that excessive stanza - // processing inside a view is a "code smell". - // Instead stanza processing should happen inside the - // models/collections. - if (pres.getAttribute('type') === 'error') { - this.showErrorMessageFromPresence(pres); - } else { - // Instead of doing it this way, we could perhaps rather - // create StatusMessage objects inside the messages - // Collection and then simply render those. Then stanza - // processing is done on the model and rendering in the - // view(s). - this.showStatusMessages(pres); - } - }, - /** * Renders a form given an IQ stanza containing the current * groupchat configuration. @@ -1246,32 +1135,6 @@ converse.plugins.add('converse-muc-views', { } }, - onNicknameClash (presence) { - /* When the nickname is already taken, we either render a - * form for the user to choose a new nickname, or we - * try to make the nickname unique by adding an integer to - * it. So john will become john-2, and then john-3 and so on. - * - * Which option is take depends on the value of - * muc_nickname_from_jid. - */ - if (_converse.muc_nickname_from_jid) { - const nick = presence.getAttribute('from').split('/')[1]; - if (nick === _converse.getDefaultMUCNickname()) { - this.model.join(nick + '-2'); - } else { - const del= nick.lastIndexOf("-"); - const num = nick.substring(del+1, nick.length); - this.model.join(nick.substring(0, del+1) + String(Number(num)+1)); - } - } else { - this.renderNicknameForm( - __("The nickname you chose is reserved or "+ - "currently in use, please choose a different one.") - ); - } - }, - hideChatRoomContents () { const container_el = this.el.querySelector('.chatroom-body'); if (!_.isNull(container_el)) { @@ -1279,9 +1142,11 @@ converse.plugins.add('converse-muc-views', { } }, - renderNicknameForm (message='') { + renderNicknameForm () { /* Render a form which allows the user to choose theirnickname. */ + const message = this.model.get('nickname_validation_message'); + this.model.save('nickname_validation_message', undefined); this.hideChatRoomContents(); if (!this.nickname_form) { this.nickname_form = new _converse.MUCNicknameForm({ @@ -1297,8 +1162,11 @@ converse.plugins.add('converse-muc-views', { u.safeSave(this.model, {'connection_status': converse.ROOMSTATUS.NICKNAME_REQUIRED}); }, - renderPasswordForm (message='') { + 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 Backbone.Model(), @@ -1308,33 +1176,32 @@ converse.plugins.add('converse-muc-views', { const container_el = this.el.querySelector('.chatroom-body'); container_el.insertAdjacentElement('beforeend', this.password_form.el); } else { - this.model.set('validation_message', message); + this.password_form.model.set('validation_message', message); } u.showElement(this.password_form.el); this.model.save('connection_status', converse.ROOMSTATUS.PASSWORD_REQUIRED); }, - showDestroyedMessage (error) { + showDestroyedMessage () { u.hideElement(this.el.querySelector('.chat-area')); u.hideElement(this.el.querySelector('.occupants')); sizzle('.spinner', this.el).forEach(u.removeElement); + const message = this.model.get('destroyed_message'); + const reason = this.model.get('destroyed_reason'); + const moved_jid = this.model.get('moved_jid'); + this.model.save({ + 'destroyed_message': undefined, + 'destroyed_reason': undefined, + 'moved_jid': undefined + }); const container = this.el.querySelector('.disconnect-container'); - const moved_jid = _.get( - sizzle('gone[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]', error).pop(), - 'textContent' - ).replace(/^xmpp:/, '').replace(/\?join$/, ''); - const reason = _.get( - sizzle('text[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]', error).pop(), - 'textContent' - ); container.innerHTML = tpl_chatroom_destroyed({ '_': _, '__':__, 'jid': moved_jid, 'reason': reason ? `"${reason}"` : null }); - const switch_el = container.querySelector('a.switch-chat'); if (switch_el) { switch_el.addEventListener('click', ev => { @@ -1348,51 +1215,37 @@ converse.plugins.add('converse-muc-views', { u.showElement(container); }, - showDisconnectMessages (msgs) { - if (_.isString(msgs)) { - msgs = [msgs]; + showDisconnectMessage () { + const message = this.model.get('disconnection_message'); + if (!message) { + return; } u.hideElement(this.el.querySelector('.chat-area')); u.hideElement(this.el.querySelector('.occupants')); sizzle('.spinner', this.el).forEach(u.removeElement); + + const messages = [message]; + const actor = this.model.get('disconnection_actor'); + if (actor) { + messages.push(__('This action was done by %1$s.', actor)); + } + const reason = this.model.get('disconnection_reason'); + if (reason) { + messages.push(__('The reason given is: "%1$s".', reason)); + } + this.model.save({ + 'disconnection_message': undefined, + 'disconnection_reason': undefined, + 'disconnection_actor': undefined + }); const container = this.el.querySelector('.disconnect-container'); container.innerHTML = tpl_chatroom_disconnect({ '_': _, - 'disconnect_messages': msgs + 'disconnect_messages': messages }) u.showElement(container); }, - /** - * @private - * @method _converse.ChatRoomView#getMessageFromStatus - * @param { XMLElement } stat: A element - * @param { Boolean } is_self: Whether the element refers to the current user - * @param { XMLElement } stanza: The original stanza received - */ - getMessageFromStatus (stat, stanza, is_self) { - const code = stat.getAttribute('code'); - if (code === '110' || (code === '100' && !is_self)) { return; } - if (code in _converse.muc.info_messages) { - return _converse.muc.info_messages[code]; - } - let nick; - if (!is_self) { - if (code in _converse.muc.action_info_messages) { - nick = Strophe.getResourceFromJid(stanza.getAttribute('from')); - return __(_converse.muc.action_info_messages[code], nick); - } - } else if (code in _converse.muc.new_nickname_messages) { - if (is_self && code === "210") { - nick = Strophe.getResourceFromJid(stanza.getAttribute('from')); - } else if (is_self && code === "303") { - nick = stanza.querySelector('x item').getAttribute('nick'); - } - return __(_converse.muc.new_nickname_messages[code], nick); - } - return; - }, - getNotificationWithMessage (message) { let el = this.content.lastElementChild; while (!_.isNil(el)) { @@ -1407,49 +1260,6 @@ converse.plugins.add('converse-muc-views', { } }, - parseXUserElement (x, stanza, is_self) { - /* Parse the passed-in - * element and construct a map containing relevant - * information. - */ - // 1. Get notification messages based on the elements. - const statuses = x.querySelectorAll('status'); - const mapper = _.partial(this.getMessageFromStatus, _, stanza, is_self); - const notification = {}; - const messages = _.reject( - _.reject(_.map(statuses, mapper), _.isUndefined), - message => this.getNotificationWithMessage(message) - ); - if (messages.length) { - notification.messages = messages; - } - // 2. Get disconnection messages based on the elements - const codes = _.invokeMap(statuses, Element.prototype.getAttribute, 'code'); - const disconnection_codes = _.intersection(codes, Object.keys(_converse.muc.disconnect_messages)); - const disconnected = is_self && disconnection_codes.length > 0; - if (disconnected) { - notification.disconnected = true; - notification.disconnection_message = _converse.muc.disconnect_messages[disconnection_codes[0]]; - } - // 3. Find the reason and actor from the element - const item = x.querySelector('item'); - // By using querySelector above, we assume here there is - // one per - // element. This appears to be a safe assumption, since - // each element pertains to a single user. - if (!_.isNull(item)) { - const reason = item.querySelector('reason'); - if (reason) { - notification.reason = reason ? reason.textContent : undefined; - } - const actor = item.querySelector('actor'); - if (actor) { - notification.actor = actor ? actor.getAttribute('nick') : undefined; - } - } - return notification; - }, - insertNotification (message) { this.content.insertAdjacentHTML( 'beforeend', @@ -1461,33 +1271,6 @@ converse.plugins.add('converse-muc-views', { ); }, - showNotificationsforUser (notification) { - /* Given the notification object generated by - * parseXUserElement, display any relevant messages and - * information to the user. - */ - if (notification.disconnected) { - const messages = []; - messages.push(notification.disconnection_message); - if (notification.actor) { - messages.push(__('This action was done by %1$s.', notification.actor)); - } - if (notification.reason) { - messages.push(__('The reason given is: "%1$s".', notification.reason)); - } - this.showDisconnectMessages(messages); - this.model.save('connection_status', converse.ROOMSTATUS.DISCONNECTED); - return; - } - if (_.get(notification.messages, 'length')) { - notification.messages.forEach(message => this.insertNotification(message)); - this.scrollDown(); - } - if (notification.reason) { - this.showChatEvent(__('The reason given is: "%1$s".', notification.reason)); - } - }, - onOccupantAdded (occupant) { if (occupant.get('show') === 'online') { this.showJoinNotification(occupant); @@ -1644,56 +1427,6 @@ converse.plugins.add('converse-muc-views', { this.scrollDown(); }, - /** - * Check for status codes and communicate their purpose to the user. - * See: https://xmpp.org/registrar/mucstatus.html - * @private - * @method _converse.ChatRoomView#showStatusMessages - * @param { XMLElement } stanza - The message or presence stanza containing the status codes - */ - showStatusMessages (stanza) { - const elements = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"]`, stanza); - const is_self = stanza.querySelectorAll("status[code='110']").length; - const iteratee = _.partial(this.parseXUserElement.bind(this), _, stanza, is_self); - const notifications = _.reject(_.map(elements, iteratee), _.isEmpty); - notifications.forEach(n => this.showNotificationsforUser(n)); - }, - - showErrorMessageFromPresence (presence) { - // We didn't enter the groupchat, so we must remove it from the MUC add-on - const error = presence.querySelector('error'); - if (error.getAttribute('type') === 'auth') { - if (!_.isNull(error.querySelector('not-authorized'))) { - this.renderPasswordForm(__("Password incorrect")); - } else if (!_.isNull(error.querySelector('registration-required'))) { - this.showDisconnectMessages(__('You are not on the member list of this groupchat.')); - } else if (!_.isNull(error.querySelector('forbidden'))) { - this.showDisconnectMessages(__('You have been banned from this groupchat.')); - } - } else if (error.getAttribute('type') === 'cancel') { - if (!_.isNull(error.querySelector('not-allowed'))) { - this.showDisconnectMessages(__('You are not allowed to create new groupchats.')); - } else if (!_.isNull(error.querySelector('not-acceptable'))) { - this.showDisconnectMessages(__("Your nickname doesn't conform to this groupchat's policies.")); - } else if (sizzle('gone[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]', error).length) { - this.showDestroyedMessage(error); - } else if (!_.isNull(error.querySelector('conflict'))) { - this.onNicknameClash(presence); - } else if (!_.isNull(error.querySelector('item-not-found'))) { - this.showDisconnectMessages(__("This groupchat does not (yet) exist.")); - } else if (!_.isNull(error.querySelector('service-unavailable'))) { - this.showDisconnectMessages(__("This groupchat has reached its maximum number of participants.")); - } else if (!_.isNull(error.querySelector('remote-server-not-found'))) { - const messages = [__("Remote server not found")]; - const reason = _.get(error.querySelector('text'), 'textContent'); - if (reason) { - messages.push(__('The explanation given is: "%1$s".', reason)); - } - this.showDisconnectMessages(messages); - } - } - }, - renderAfterTransition () { /* Rerender the groupchat after some kind of transition. For * example after the spinner has been removed or after a diff --git a/src/headless/converse-muc.js b/src/headless/converse-muc.js index 71841b514..fd1d87b7f 100644 --- a/src/headless/converse-muc.js +++ b/src/headless/converse-muc.js @@ -61,7 +61,8 @@ converse.ROOMSTATUS = { NICKNAME_REQUIRED: 2, PASSWORD_REQUIRED: 3, DISCONNECTED: 4, - ENTERED: 5 + ENTERED: 5, + DESTROYED: 6 }; @@ -124,6 +125,76 @@ converse.plugins.add('converse-muc', { } + function ___ (str) { + /* This is part of a hack to get gettext to scan strings to be + * translated. Strings we cannot send to the function above because + * they require variable interpolation and we don't yet have the + * variables at scan time. + */ + return str; + } + + /* https://xmpp.org/extensions/xep-0045.html + * ---------------------------------------- + * 100 message Entering a groupchat Inform user that any occupant is allowed to see the user's full JID + * 101 message (out of band) Affiliation change Inform user that his or her affiliation changed while not in the groupchat + * 102 message Configuration change Inform occupants that groupchat now shows unavailable members + * 103 message Configuration change Inform occupants that groupchat now does not show unavailable members + * 104 message Configuration change Inform occupants that a non-privacy-related groupchat configuration change has occurred + * 110 presence Any groupchat presence Inform user that presence refers to one of its own groupchat occupants + * 170 message or initial presence Configuration change Inform occupants that groupchat logging is now enabled + * 171 message Configuration change Inform occupants that groupchat logging is now disabled + * 172 message Configuration change Inform occupants that the groupchat is now non-anonymous + * 173 message Configuration change Inform occupants that the groupchat is now semi-anonymous + * 174 message Configuration change Inform occupants that the groupchat is now fully-anonymous + * 201 presence Entering a groupchat Inform user that a new groupchat has been created + * 210 presence Entering a groupchat Inform user that the service has assigned or modified the occupant's roomnick + * 301 presence Removal from groupchat Inform user that he or she has been banned from the groupchat + * 303 presence Exiting a groupchat Inform all occupants of new groupchat nickname + * 307 presence Removal from groupchat Inform user that he or she has been kicked from the groupchat + * 321 presence Removal from groupchat Inform user that he or she is being removed from the groupchat because of an affiliation change + * 322 presence Removal from groupchat Inform user that he or she is being removed from the groupchat because the groupchat has been changed to members-only and the user is not a member + * 332 presence Removal from groupchat Inform user that he or she is being removed from the groupchat because of a system shutdown + */ + _converse.muc = { + info_messages: { + 100: __('This groupchat is not anonymous'), + 102: __('This groupchat now shows unavailable members'), + 103: __('This groupchat does not show unavailable members'), + 104: __('The groupchat configuration has changed'), + 170: __('groupchat logging is now enabled'), + 171: __('groupchat logging is now disabled'), + 172: __('This groupchat is now no longer anonymous'), + 173: __('This groupchat is now semi-anonymous'), + 174: __('This groupchat is now fully-anonymous'), + 201: __('A new groupchat has been created') + }, + + new_nickname_messages: { + // XXX: Note the triple underscore function and not double underscore. + 210: ___('Your nickname has been automatically set to %1$s'), + 303: ___('Your nickname has been changed to %1$s') + }, + + disconnect_messages: { + 301: __('You have been banned from this groupchat'), + 307: __('You have been kicked from this groupchat'), + 321: __("You have been removed from this groupchat because of an affiliation change"), + 322: __("You have been removed from this groupchat because the groupchat has changed to members-only and you're not a member"), + 332: __("You have been removed from this groupchat because the service hosting it is being shut down") + }, + + action_info_messages: { + // XXX: Note the triple underscore function and not double underscore. + 301: ___("%1$s has been banned"), + 303: ___("%1$s's nickname has changed"), + 307: ___("%1$s has been kicked out"), + 321: ___("%1$s has been removed because of an affiliation change"), + 322: ___("%1$s has been removed for not being a member") + } + } + + async function openRoom (jid) { if (!u.isValidMUCJID(jid)) { return _converse.log( @@ -300,7 +371,6 @@ converse.plugins.add('converse-muc', { const room_jid = this.get('jid'); this.removeHandlers(); this.presence_handler = _converse.connection.addHandler(stanza => { - Object.values(this.handlers.presence).forEach(callback => callback(stanza)); this.onPresence(stanza); return true; }, @@ -308,7 +378,6 @@ converse.plugins.add('converse-muc', { {'ignoreNamespaceFragment': true, 'matchBareFromJid': true} ); this.message_handler = _converse.connection.addHandler(stanza => { - Object.values(this.handlers.message).forEach(callback => callback(stanza)); this.onMessage(stanza); return true; }, null, 'message', 'groupchat', null, room_jid, @@ -331,21 +400,6 @@ converse.plugins.add('converse-muc', { return this; }, - addHandler (type, name, callback) { - /* Allows 'presence' and 'message' handlers to be - * registered. These will be executed once presence or - * message stanzas are received, and *before* this model's - * own handlers are executed. - */ - if (_.isNil(this.handlers)) { - this.handlers = {}; - } - if (_.isNil(this.handlers[type])) { - this.handlers[type] = {}; - } - this.handlers[type][name] = callback; - }, - getDisplayName () { const name = this.get('name'); if (name) { @@ -1013,9 +1067,9 @@ converse.plugins.add('converse-muc', { }).c('query', {'xmlns': Strophe.NS.MUC_REGISTER}) ); } catch (e) { - if (sizzle('not-allowed[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]', e).length) { + if (sizzle(`not-allowed[xmlns="${Strophe.NS.STANZAS}"]`, e).length) { err_msg = __("You're not allowed to register yourself in this groupchat."); - } else if (sizzle('registration-required[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]', e).length) { + } else if (sizzle(`registration-required[xmlns="${Strophe.NS.STANZAS}"]`, e).length) { err_msg = __("You're not allowed to register in this groupchat because it's members-only."); } _converse.log(e, Strophe.LogLevel.ERROR); @@ -1036,9 +1090,9 @@ converse.plugins.add('converse-muc', { .c('field', {'var': 'muc#register_roomnick'}).c('value').t(nick) ); } catch (e) { - if (sizzle('service-unavailable[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]', e).length) { + if (sizzle(`service-unavailable[xmlns="${Strophe.NS.STANZAS}"]`, e).length) { err_msg = __("Can't register your nickname in this groupchat, it doesn't support registration."); - } else if (sizzle('bad-request[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]', e).length) { + } else if (sizzle(`bad-request[xmlns="${Strophe.NS.STANZAS}"]`, e).length) { err_msg = __("Can't register your nickname in this groupchat, invalid data form supplied."); } _converse.log(err_msg); @@ -1320,6 +1374,7 @@ converse.plugins.add('converse-muc', { * @param { XMLElement } stanza - The message stanza. */ async onMessage (stanza) { + this.createInfoMessages(stanza); this.fetchFeaturesIfConfigurationChanged(stanza); const original_stanza = stanza; const forwarded = sizzle(`forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).pop(); @@ -1360,21 +1415,168 @@ converse.plugins.add('converse-muc', { } }, + handleDisconnection (stanza) { + const is_self = !_.isNull(stanza.querySelector("status[code='110']")); + const x = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"]`, stanza).pop(); + if (!x) { + return; + } + const codes = sizzle('status', x).map(s => s.getAttribute('code')); + const disconnection_codes = _.intersection(codes, Object.keys(_converse.muc.disconnect_messages)); + const disconnected = is_self && disconnection_codes.length > 0; + if (!disconnected) { + return; + } + // By using querySelector we assume here there is + // one per + // element. This appears to be a safe assumption, since + // each element pertains to a single user. + const item = x.querySelector('item'); + const reason = item ? _.get(item.querySelector('reason'), 'textContent') : undefined; + const actor = item ? _.invoke(item.querySelector('actor'), 'getAttribute', 'nick') : undefined; + const message = _converse.muc.disconnect_messages[disconnection_codes[0]]; + this.setDisconnectionMessage(message, reason, actor); + }, - onErrorPresence (pres) { - // TODO: currently showErrorMessageFromPresence handles - // 'error" presences in converse-muc-views. - // Instead, they should be handled here and the presence - // handler removed from there. - if (sizzle(`error not-authorized[xmlns="${Strophe.NS.STANZAS}"]`, pres).length) { - this.save('connection_status', converse.ROOMSTATUS.PASSWORD_REQUIRED); - } else if (sizzle(`error[type="modify"]`, pres).length) { - this.handleModifyError(pres); + + /** + * Create info messages based on a received presence stanza + * @private + * @method _converse.ChatRoom#createInfoMessages + * @param { XMLElement } stanza: The presence stanza received + */ + createInfoMessages (stanza) { + const is_self = !_.isNull(stanza.querySelector("status[code='110']")); + const x = sizzle(`x[xmlns="${Strophe.NS.MUC_USER}"]`, stanza).pop(); + if (!x) { + return; + } + const codes = sizzle('status', x).map(s => s.getAttribute('code')); + + codes.forEach(code => { + let message; + if (code === '110' || (code === '100' && !is_self)) { + return; + } else if (code in _converse.muc.info_messages) { + message = _converse.muc.info_messages[code]; + + } else if (!is_self && (code in _converse.muc.action_info_messages)) { + const nick = Strophe.getResourceFromJid(stanza.getAttribute('from')); + message = __(_converse.muc.action_info_messages[code], nick); + const item = x.querySelector('item'); + const reason = item ? _.get(item.querySelector('reason'), 'textContent') : undefined; + const actor = item ? _.invoke(item.querySelector('actor'), 'getAttribute', 'nick') : undefined; + if (actor) { + message += '\n' + __('This action was done by %1$s.', actor); + } + if (reason) { + message += '\n' + __('The reason given is: "%1$s".', reason); + } + } else if (is_self && (code in _converse.muc.new_nickname_messages)) { + let nick; + if (is_self && code === "210") { + nick = Strophe.getResourceFromJid(stanza.getAttribute('from')); + } else if (is_self && code === "303") { + nick = stanza.querySelector('x item').getAttribute('nick'); + } + this.save('nick', nick); + message = __(_converse.muc.new_nickname_messages[code], nick); + } + + if (message) { + this.messages.create({'type': 'info', message}); + } + }); + }, + + + setDisconnectionMessage (message, reason, actor) { + this.save({ + 'connection_status': converse.ROOMSTATUS.DISCONNECTED, + 'disconnection_message': message, + 'disconnection_reason': reason, + 'disconnection_actor': actor + }); + }, + + + onNicknameClash (presence) { + if (_converse.muc_nickname_from_jid) { + const nick = presence.getAttribute('from').split('/')[1]; + if (nick === _converse.getDefaultMUCNickname()) { + this.join(nick + '-2'); + } else { + const del= nick.lastIndexOf("-"); + const num = nick.substring(del+1, nick.length); + this.join(nick.substring(0, del+1) + String(Number(num)+1)); + } } else { - this.save('connection_status', converse.ROOMSTATUS.DISCONNECTED); + this.save({ + 'nickname_validation_message': __( + "The nickname you chose is reserved or "+ + "currently in use, please choose a different one."), + 'connection_status': converse.ROOMSTATUS.NICKNAME_REQUIRED + }); } }, + + /** + * Parses a stanza with type "error" and sets the proper + * `connection_status` value for this {@link _converse.ChatRoom} as + * well as any additional output that can be shown to the user. + * @private + * @param { XMLElement } stanza - The presence stanza + */ + onErrorPresence (stanza) { + if (sizzle(`error not-authorized[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length) { + this.save({ + 'password_validation_message': __("Password incorrect"), + 'connection_status': converse.ROOMSTATUS.PASSWORD_REQUIRED + }); + } + const error = stanza.querySelector('error'); + const error_type = error.getAttribute('type'); + + if (error_type === 'modify') { + this.handleModifyError(stanza); + } else if (error_type === 'auth') { + if (error.querySelector('registration-required')) { + this.setDisconnectionMessage(__('You are not on the member list of this groupchat.')); + } else if (error.querySelector('forbidden')) { + this.setDisconnectionMessage(__('You have been banned from this groupchat.')); + } + } else if (error_type === 'cancel') { + if (error.querySelector('not-allowed')) { + this.setDisconnectionMessage(__('You are not allowed to create new groupchats.')); + } else if (error.querySelector('not-acceptable')) { + this.setDisconnectionMessage(__("Your nickname doesn't conform to this groupchat's policies.")); + } else if (sizzle(`gone[xmlns="${Strophe.NS.STANZAS}"]`, error).length) { + const moved_jid = _.get(sizzle(`gone[xmlns="${Strophe.NS.STANZAS}"]`, error).pop(), 'textContent') + .replace(/^xmpp:/, '') + .replace(/\?join$/, ''); + const reason = _.get(sizzle(`text[xmlns="${Strophe.NS.STANZAS}"]`, error).pop(), 'textContent'); + this.save({ + 'connection_status': converse.ROOMSTATUS.DESTROYED, + 'destroyed_reason': reason, + 'moved_jid': moved_jid + }); + } else if (error.querySelector('conflict')) { + this.onNicknameClash(stanza); + } else if (error.querySelector('item-not-found')) { + this.setDisconnectionMessage(__("This groupchat does not (yet) exist.")); + } else if (error.querySelector('service-unavailable')) { + this.setDisconnectionMessage(__("This groupchat has reached its maximum number of participants.")); + } else if (error.querySelector('remote-server-not-found')) { + const message = __("Remote server not found"); + const text = _.get(error.querySelector('text'), 'textContent'); + const reason = text ? __('The explanation given is: "%1$s".', text) : undefined; + this.setDisconnectionMessage(message, reason); + } + } + }, + + /** * Handles all MUC presence stanzas. * @private @@ -1388,6 +1590,7 @@ converse.plugins.add('converse-muc', { if (stanza.querySelector("status[code='110']")) { this.onOwnPresence(stanza); } + this.createInfoMessages(stanza); this.updateOccupantsOnPresence(stanza); if (this.get('role') !== 'none' && this.get('connection_status') === converse.ROOMSTATUS.CONNECTING) { @@ -1414,7 +1617,7 @@ converse.plugins.add('converse-muc', { this.saveAffiliationAndRole(stanza); if (stanza.getAttribute('type') === 'unavailable') { - this.save('connection_status', converse.ROOMSTATUS.DISCONNECTED); + this.handleDisconnection(stanza); } else { const locked_room = stanza.querySelector("status[code='201']"); if (locked_room) { diff --git a/tests/utils.js b/tests/utils.js index ef1833bf8..959f65671 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -114,8 +114,10 @@ return _converse.chatboxviews.get(jid); }; - utils.openChatRoom = function (_converse, room, server) { - return _converse.api.rooms.open(`${room}@${server}`); + utils.openChatRoom = async function (_converse, room, server) { + const model = await _converse.api.rooms.open(`${room}@${server}`); + await model.messages.fetched; + return model; }; utils.getRoomFeatures = async function (_converse, room, server, features=[]) {