From 9528d81c00bc4a876fecbd5cb72cac0f35f24b75 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 8 Apr 2018 19:44:53 +0200 Subject: [PATCH] Move various MUC methods onto the Backbone.Model To more cleanly separate views and models and to make MUC in headless mode more viable. Refs #1032 --- CHANGES.md | 16 +- css/converse.css | 3 + css/inverse.css | 59 ++-- docs/source/events.rst | 17 +- sass/_chatbox.scss | 3 + sass/inverse/_chatbox.scss | 1 - spec/chatroom.js | 460 +++++++++++++------------ spec/mam.js | 2 +- spec/minchats.js | 2 +- spec/notification.js | 6 +- spec/roomslist.js | 6 +- src/converse-chatview.js | 1 + src/converse-mam.js | 42 +-- src/converse-muc-views.js | 678 +++++++++---------------------------- src/converse-muc.js | 621 ++++++++++++++++++++++++++------- src/converse-roomslist.js | 3 +- 16 files changed, 996 insertions(+), 924 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 37697061a..5e39f4bcc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,8 +4,8 @@ ## UI changes -* The UI is now based on Bootstrap4 and Flexbox is used extensively. -* #956 Conversation pane should show my own identity in pane header +- The UI is now based on Bootstrap4 and Flexbox is used extensively. +- #956 Conversation pane should show my own identity in pane header ## New Features @@ -13,15 +13,21 @@ ## Configuration changes -* Removed the `xhr_custom_status` and `xhr_custom_status_url` configuration +- Removed the `xhr_custom_status` and `xhr_custom_status_url` configuration settings. If you relied on these settings, you can instead listen for the [statusMessageChanged](https://conversejs.org/docs/html/events.html#contactstatusmessagechanged) event and make the XMLHttpRequest yourself. -* Removed `xhr_user_search` in favor of only accepting `xhr_user_search_url` as configuration option. -* The data returned from the `xhr_user_search_url` must now include the user's +- Removed `xhr_user_search` in favor of only accepting `xhr_user_search_url` as configuration option. +- The data returned from the `xhr_user_search_url` must now include the user's `jid` instead of just an `id`. - New configuration setting [nickname](https://conversejs.org/docs/html/configurations.html#nickname) +## Architectural changes + +- Extracted the views from `converse-muc.js` into `converse-muc-views.js` and + where appropriate moved methods from the views into the models/collections. + This makes MUC possible in headless mode. + ### Bugfixes - Spoiler messages didn't include the message author's name. diff --git a/css/converse.css b/css/converse.css index e4633b404..4f613ac49 100644 --- a/css/converse.css +++ b/css/converse.css @@ -7594,6 +7594,9 @@ body.reset { #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li .toolbar-menu ul li.insert-emoji a:hover, #conversejs .chatbox .sendXMPPMessage .chat-toolbar li .toolbar-menu ul li.insert-emoji a:hover { color: #8f2831; } + #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley a.toggle-smiley, + #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley a.toggle-smiley { + padding: 0; } #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar, #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar { box-shadow: 0 -1px 1px 0 rgba(0, 0, 0, 0.4); } diff --git a/css/inverse.css b/css/inverse.css index 545f93afa..90c791ba5 100644 --- a/css/inverse.css +++ b/css/inverse.css @@ -7647,28 +7647,31 @@ body { #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li .toolbar-menu ul li.insert-emoji a:hover, #conversejs .chatbox .sendXMPPMessage .chat-toolbar li .toolbar-menu ul li.insert-emoji a:hover { color: #8f2831; } - #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar, - #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar { - box-shadow: 0 -1px 1px 0 rgba(0, 0, 0, 0.4); } - #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker, - #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker { - padding-top: 0.5em; } - #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker ul, - #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker ul { - display: flex; - flex-direction: row; - justify-content: space-between; } - #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker li, - #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-skintone-picker li, - #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker li, - #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-skintone-picker li { - padding: 0.2em; - font-size: 26px; } - #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker li:hover, - #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-skintone-picker li:hover, - #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker li:hover, - #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-skintone-picker li:hover { - background-color: #DCF9F6; } + #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley, + #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley { + padding: 0 0 0 0.5em; } + #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar, + #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar { + box-shadow: 0 -1px 1px 0 rgba(0, 0, 0, 0.4); } + #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker, + #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker { + padding-top: 0.5em; } + #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker ul, + #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker ul { + display: flex; + flex-direction: row; + justify-content: space-between; } + #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker li, + #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-skintone-picker li, + #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker li, + #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-skintone-picker li { + padding: 0.2em; + font-size: 26px; } + #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker li:hover, + #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-skintone-picker li:hover, + #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-category-picker li:hover, + #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-smiley .emoji-toolbar .emoji-skintone-picker li:hover { + background-color: #DCF9F6; } #converse-embedded-chat .chatbox .sendXMPPMessage .chat-toolbar li.toggle-otr ul, #conversejs .chatbox .sendXMPPMessage .chat-toolbar li.toggle-otr ul { z-index: 99; } @@ -7788,13 +7791,11 @@ body { line-height: 26px; } #conversejs.fullscreen .chatbox .sendXMPPMessage ul { width: 100%; } - #conversejs.fullscreen .chatbox .sendXMPPMessage .toggle-smiley { - padding-left: 0.5em; } - #conversejs.fullscreen .chatbox .sendXMPPMessage .toggle-smiley ul.emoji-toolbar .emoji-category-picker { - margin-right: 5em; } - #conversejs.fullscreen .chatbox .sendXMPPMessage .toggle-smiley ul.emoji-toolbar .emoji-category { - padding-left: 10px; - padding-right: 10px; } + #conversejs.fullscreen .chatbox .sendXMPPMessage .toggle-smiley ul.emoji-toolbar .emoji-category-picker { + margin-right: 5em; } + #conversejs.fullscreen .chatbox .sendXMPPMessage .toggle-smiley ul.emoji-toolbar .emoji-category { + padding-left: 10px; + padding-right: 10px; } @media screen and (max-width: 767px) { #conversejs.fullscreen .chatbox { diff --git a/docs/source/events.rst b/docs/source/events.rst index 56f6f8574..861953394 100644 --- a/docs/source/events.rst +++ b/docs/source/events.rst @@ -34,8 +34,8 @@ For more info on how to use (or add promises), you can read the Below we will now list all events and also specify whether they are available as promises. -List of Events (and promises) ------------------------------ +List of global events (and promises) +------------------------------------ Hooking into events that Converse.js emits is a great way to extend or customize its functionality. @@ -478,3 +478,16 @@ windowStateChanged When window state has changed. Used to determine when a user left the page and when came back. ``_converse.on('windowStateChanged', function (data) { ... });`` + + +List of events on the ChatRoom Backbone.Model +--------------------------------------------- + +configurationNeeded +~~~~~~~~~~~~~~~~~~~ + +Triggered when a new room has been created which first needs to be configured +and when `auto_configure` is set to `false`. + +Used by the core `ChatRoomView` view in order to know when to render the +configuration form for a new room. diff --git a/sass/_chatbox.scss b/sass/_chatbox.scss index 5525ff249..23defe7a4 100644 --- a/sass/_chatbox.scss +++ b/sass/_chatbox.scss @@ -433,6 +433,9 @@ } } &.toggle-smiley { + a.toggle-smiley { + padding: 0; + } .emoji-toolbar { box-shadow: 0 -1px 1px 0 rgba(0, 0, 0, 0.4); diff --git a/sass/inverse/_chatbox.scss b/sass/inverse/_chatbox.scss index 81be6fd6b..99dd56211 100644 --- a/sass/inverse/_chatbox.scss +++ b/sass/inverse/_chatbox.scss @@ -78,7 +78,6 @@ width: 100%; } .toggle-smiley { - padding-left: 0.5em; ul { &.emoji-toolbar { .emoji-category-picker { diff --git a/spec/chatroom.js b/spec/chatroom.js index 454c091ef..34a9f0411 100644 --- a/spec/chatroom.js +++ b/spec/chatroom.js @@ -127,7 +127,7 @@ // Mock 'getRoomFeatures', otherwise the room won't be // displayed as it waits first for the features to be returned // (when it's a new room being created). - spyOn(_converse.ChatRoomView.prototype, 'getRoomFeatures').and.callFake(function () { + spyOn(_converse.ChatRoom.prototype, 'getRoomFeatures').and.callFake(function () { var deferred = new $.Deferred(); deferred.resolve(); return deferred.promise(); @@ -426,7 +426,7 @@ * know about them because we receive their presences before we * receive our own. */ - presence = $pres({ + var presence = $pres({ to: 'dummy@localhost/_converse.js-29092160', from: 'coven@chat.shakespeare.lit/oldguy' }).c('x', {xmlns: Strophe.NS.MUC_USER}) @@ -446,7 +446,7 @@ * * */ - var presence = $pres({ + presence = $pres({ to: 'dummy@localhost/_converse.js-29092160', from: 'coven@chat.shakespeare.lit/some1' }).c('x', {xmlns: Strophe.NS.MUC_USER}) @@ -615,140 +615,159 @@ null, ['rosterGroupsFetched'], {}, function (done, _converse) { - test_utils.openChatRoom(_converse, "coven", 'chat.shakespeare.lit', 'some1'); - var view = _converse.chatboxviews.get('coven@chat.shakespeare.lit'); - var $chat_content = $(view.el).find('.chat-content'); + test_utils.openAndEnterChatRoom(_converse, 'coven', 'chat.shakespeare.lit', 'dummy').then(function () { + var view = _converse.chatboxviews.get('coven@chat.shakespeare.lit'); + var chat_content = view.el.querySelector('.chat-content'); + var $chat_content = $(chat_content); + var time = chat_content.querySelector('time'); + expect(time).not.toBe(null); + expect(time.getAttribute('class')).toEqual('message chat-info chat-date badge badge-info'); + expect(time.getAttribute('data-isodate')).toEqual(moment().startOf('day').format()); + expect(time.textContent).toEqual(moment().startOf('day').format("dddd MMM Do YYYY")); + expect(chat_content.querySelectorAll('div.chat-info').length).toBe(1); + expect(chat_content.querySelector('div.chat-info').textContent).toBe( + "dummy has entered the room" + ); - /* - * - * - * - * - * - */ - var presence = $pres({ - to: 'dummy@localhost/_converse.js-29092160', - from: 'coven@chat.shakespeare.lit/some1' - }).c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'owner', - 'jid': 'dummy@localhost/_converse.js-29092160', - 'role': 'moderator' - }).up() - .c('status', {code: '110'}); - _converse.connection._dataRecv(test_utils.createRequest(presence)); + var baseTime = new Date(); + jasmine.clock().install(); + jasmine.clock().mockDate(baseTime); + var ONE_DAY_LATER = 86400000; + jasmine.clock().tick(ONE_DAY_LATER); - var $time = $chat_content.find('time'); - expect($time.length).toEqual(1); - expect($time.attr('class')).toEqual('message chat-info chat-date badge badge-info'); - expect($time.data('isodate')).toEqual(moment().startOf('day').format()); - expect($time.text()).toEqual(moment().startOf('day').format("dddd MMM Do YYYY")); - expect($chat_content.find('div.chat-info:first').html()).toBe("some1 has entered the room"); + /* + * + * + * + * + * + */ + var presence = $pres({ + to: 'dummy@localhost/_converse.js-29092160', + from: 'coven@chat.shakespeare.lit/some1' + }).c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'owner', + 'jid': 'some1@localhost/_converse.js-290929789', + 'role': 'moderator' + }); + _converse.connection._dataRecv(test_utils.createRequest(presence)); - // XXX: Hack. We clear the chat contents instead of mocking the date - $chat_content.html(''); + time = chat_content.querySelector('time[data-isodate="'+moment().startOf('day').format()+'"]'); + expect(time).not.toBe(null); + expect(time.getAttribute('class')).toEqual('message chat-info chat-date badge badge-info'); + expect(time.getAttribute('data-isodate')).toEqual(moment().startOf('day').format()); + expect(time.textContent).toEqual(moment().startOf('day').format("dddd MMM Do YYYY")); + expect(chat_content.querySelector('div.chat-info:last-child').textContent).toBe( + "some1 has entered the room" + ); - // Test a user leaving a chat room - presence = $pres({ - to: 'dummy@localhost/_converse.js-29092160', - type: 'unavailable', - from: 'coven@chat.shakespeare.lit/some1' - }) - .c('status', 'Disconnected: Replaced by new connection').up() - .c('x', {xmlns: Strophe.NS.MUC_USER}) + jasmine.clock().tick(ONE_DAY_LATER); + + // Test a user leaving a chat room + presence = $pres({ + to: 'dummy@localhost/_converse.js-29092160', + type: 'unavailable', + from: 'coven@chat.shakespeare.lit/some1' + }) + .c('status', 'Disconnected: Replaced by new connection').up() + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'some1@localhost/_converse.js-290929789', + 'role': 'none' + }); + _converse.connection._dataRecv(test_utils.createRequest(presence)); + + time = chat_content.querySelector('time[data-isodate="'+moment().startOf('day').format()+'"]'); + expect(time).not.toBe(null); + expect(time.getAttribute('class')).toEqual('message chat-info chat-date badge badge-info'); + expect(time.getAttribute('data-isodate')).toEqual(moment().startOf('day').format()); + expect(time.textContent).toEqual(moment().startOf('day').format("dddd MMM Do YYYY")); + expect($(chat_content).find('div.chat-info').length).toBe(4); + expect($(chat_content).find('div.chat-info:last').html()).toBe( + 'some1 has left the room. '+ + '"Disconnected: Replaced by new connection"'); + + jasmine.clock().tick(ONE_DAY_LATER); + + var stanza = Strophe.xmlHtmlNode( + ''+ + ' hello world'+ + ' '+ + '').firstChild; + _converse.connection._dataRecv(test_utils.createRequest(stanza)); + + presence = $pres({ + to: 'dummy@localhost/_converse.js-29092160', + from: 'coven@chat.shakespeare.lit/newguy' + }).c('x', {xmlns: Strophe.NS.MUC_USER}) .c('item', { 'affiliation': 'none', - 'jid': 'some1@localhost/_converse.js-290929789', - 'role': 'none' + 'jid': 'newguy@localhost/_converse.js-290929789', + 'role': 'participant' }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); + _converse.connection._dataRecv(test_utils.createRequest(presence)); - $time = $chat_content.find('time'); - expect($time.length).toEqual(1); - expect($time.attr('class')).toEqual('message chat-info chat-date badge badge-info'); - expect($time.data('isodate')).toEqual(moment().startOf('day').format()); - expect($time.text()).toEqual(moment().startOf('day').format("dddd MMM Do YYYY")); - expect($chat_content.find('div.chat-info').length).toBe(1); - expect($chat_content.find('div.chat-info:last').html()).toBe( - 'some1 has left the room. '+ - '"Disconnected: Replaced by new connection"'); + var $time = $chat_content.find('time'); + expect($time.length).toEqual(4); - // XXX: Hack. We clear the chat contents instead of mocking the date - $chat_content.html(''); + $time = $chat_content.find('time:eq(3)'); + expect($time.attr('class')).toEqual('message chat-info chat-date badge badge-info'); + expect($time.data('isodate')).toEqual(moment().startOf('day').format()); + expect($time.text()).toEqual(moment().startOf('day').format("dddd MMM Do YYYY")); + expect($chat_content.find('div.chat-info').length).toBe(5); + expect($chat_content.find('div.chat-info:last').html()).toBe("newguy has entered the room"); - var stanza = Strophe.xmlHtmlNode( - ''+ - ' hello world'+ - ' '+ - '').firstChild; - _converse.connection._dataRecv(test_utils.createRequest(stanza)); + jasmine.clock().tick(ONE_DAY_LATER); - presence = $pres({ - to: 'dummy@localhost/_converse.js-29092160', - from: 'coven@chat.shakespeare.lit/newguy' - }).c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'newguy@localhost/_converse.js-290929789', - 'role': 'participant' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); + stanza = Strophe.xmlHtmlNode( + ''+ + ' hello world'+ + ' '+ + '').firstChild; + _converse.connection._dataRecv(test_utils.createRequest(stanza)); - $time = $chat_content.find('time'); - expect($time.length).toEqual(2); + jasmine.clock().tick(ONE_DAY_LATER); - $time = $chat_content.find('time:eq(1)'); - expect($time.attr('class')).toEqual('message chat-info chat-date badge badge-info'); - expect($time.data('isodate')).toEqual(moment().startOf('day').format()); - expect($time.text()).toEqual(moment().startOf('day').format("dddd MMM Do YYYY")); - expect($chat_content.find('div.chat-info').length).toBe(1); - expect($chat_content.find('div.chat-info:first').html()).toBe("newguy has entered the room"); + // Test a user leaving a chat room + presence = $pres({ + to: 'dummy@localhost/_converse.js-29092160', + type: 'unavailable', + from: 'coven@chat.shakespeare.lit/newguy' + }) + .c('status', 'Disconnected: Replaced by new connection').up() + .c('x', {xmlns: Strophe.NS.MUC_USER}) + .c('item', { + 'affiliation': 'none', + 'jid': 'newguy@localhost/_converse.js-290929789', + 'role': 'none' + }); + _converse.connection._dataRecv(test_utils.createRequest(presence)); - // XXX: Hack. We clear the chat contents instead of mocking the date - $chat_content.html(''); + $time = $chat_content.find('time'); + expect($time.length).toEqual(6); - stanza = Strophe.xmlHtmlNode( - ''+ - ' hello world'+ - ' '+ - '').firstChild; - _converse.connection._dataRecv(test_utils.createRequest(stanza)); + $time = $chat_content.find('time:eq(5)'); + expect($time.attr('class')).toEqual('message chat-info chat-date badge badge-info'); + expect($time.data('isodate')).toEqual(moment().startOf('day').format()); + expect($time.text()).toEqual(moment().startOf('day').format("dddd MMM Do YYYY")); + expect($chat_content.find('div.chat-info').length).toBe(6); + expect($chat_content.find('div.chat-info:last').html()).toBe( + 'newguy has left the room. '+ + '"Disconnected: Replaced by new connection"'); - // Test a user leaving a chat room - presence = $pres({ - to: 'dummy@localhost/_converse.js-29092160', - type: 'unavailable', - from: 'coven@chat.shakespeare.lit/some1' - }) - .c('status', 'Disconnected: Replaced by new connection').up() - .c('x', {xmlns: Strophe.NS.MUC_USER}) - .c('item', { - 'affiliation': 'none', - 'jid': 'some1@localhost/_converse.js-290929789', - 'role': 'none' - }); - _converse.connection._dataRecv(test_utils.createRequest(presence)); - - $time = $chat_content.find('time'); - expect($time.length).toEqual(2); - - $time = $chat_content.find('time:eq(1)'); - expect($time.attr('class')).toEqual('message chat-info chat-date badge badge-info'); - expect($time.data('isodate')).toEqual(moment().startOf('day').format()); - expect($time.text()).toEqual(moment().startOf('day').format("dddd MMM Do YYYY")); - expect($chat_content.find('div.chat-info').length).toBe(1); - expect($chat_content.find('div.chat-info:last').html()).toBe( - 'some1 has left the room. '+ - '"Disconnected: Replaced by new connection"'); - done(); - return; + jasmine.clock().uninstall(); + done(); + return; + }); })); it("shows its description in the chat heading", @@ -818,7 +837,7 @@ to: 'dummy@localhost', type: 'groupchat' }).c('body').t(message).tree(); - view.handleMUCMessage(msg); + view.model.onMessage(msg); expect($(view.el).find('.chat-message').hasClass('mentioned')).toBeTruthy(); done(); }); @@ -848,7 +867,7 @@ to: 'dummy@localhost', type: 'groupchat' }).c('body').t(message).tree(); - view.handleMUCMessage(msg); + view.model.onMessage(msg); expect(_.includes($(view.el).find('.chat-msg-author').text(), '**Dyon van de Wege')).toBeTruthy(); expect($(view.el).find('.chat-msg-content').text()).toBe(' is tired'); @@ -859,7 +878,7 @@ to: 'dummy@localhost', type: 'groupchat' }).c('body').t(message).tree(); - view.handleMUCMessage(msg); + view.model.onMessage(msg); expect(_.includes($(view.el).find('.chat-msg-author:last').text(), '**Max Mustermann')).toBeTruthy(); expect($(view.el).find('.chat-msg-content:last').text()).toBe(' is as well'); done(); @@ -1321,7 +1340,7 @@ .c('status').attrs({code:'210'}).nodeTree; _converse.connection._dataRecv(test_utils.createRequest(presence)); - var info_text = $(view.el).find('.chat-content .chat-info').text(); + var info_text = $(view.el).find('.chat-content .chat-info:first').text(); expect(info_text).toBe('Your nickname has been automatically set to thirdwitch'); done(); }); @@ -1442,7 +1461,7 @@ to: 'dummy@localhost', type: 'groupchat' }).c('body').t(text); - view.onChatRoomMessage(message.nodeTree); + view.model.onMessage(message.nodeTree); var $chat_content = $(view.el).find('.chat-content'); expect($chat_content.find('.chat-message').length).toBe(1); expect($chat_content.find('.chat-msg-content').text()).toBe(text); @@ -1480,7 +1499,7 @@ type: 'groupchat', id: view.model.messages.at(0).get('msgid') }).c('body').t(text); - view.onChatRoomMessage(message.nodeTree); + view.model.onMessage(message.nodeTree); expect($chat_content.find('.chat-message').length).toBe(1); expect($chat_content.find('.chat-msg-content').last().text()).toBe(text); // We don't emit an event if it's our own message @@ -1502,7 +1521,7 @@ * scrollbar. */ for (var i=0; i<20; i++) { - view.handleMUCMessage( + view.model.onMessage( $msg({ from: 'lounge@localhost/someone', to: 'dummy@localhost.com', @@ -1513,7 +1532,7 @@ // Give enough time for `markScrolled` to have been called setTimeout(function () { view.content.scrollTop = 0; - view.handleMUCMessage( + view.model.onMessage( $msg({ from: 'lounge@localhost/someone', to: 'dummy@localhost.com', @@ -1562,7 +1581,10 @@ spyOn(window, 'alert'); var subject = ''; var view = _converse.chatboxviews.get('jdev@conference.jabber.org'); - view.setChatRoomSubject('ralphm', subject); + view.model.set({'subject': { + 'text': subject, + 'author': 'ralphm' + }}); var chat_content = view.el.querySelector('.chat-content'); expect($(chat_content).find('.chat-event:last').text()).toBe('Topic set by ralphm'); expect($(chat_content).find('.chat-topic:last').text()).toBe(subject); @@ -1615,35 +1637,14 @@ var view = _converse.chatboxviews.get('lounge@localhost'); var $chat_content = $(view.el).find('.chat-content'); - // The user has just entered the room and receives their own - // presence from the server. - // See example 24: - // http://xmpp.org/extensions/xep-0045.html#enter-pres - var presence = $pres({ - to:'dummy@localhost/pda', - from:'lounge@localhost/oldnick', - id:'DC352437-C019-40EC-B590-AF29E879AF97' - }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) - .c('item').attrs({ - affiliation: 'member', - jid: 'dummy@localhost/pda', - role: 'participant' - }).up() - .c('status').attrs({code:'110'}).up() - .c('status').attrs({code:'210'}).nodeTree; - - _converse.connection._dataRecv(test_utils.createRequest(presence)); var $occupants = $(view.el.querySelector('.occupant-list')); expect($occupants.children().length).toBe(1); expect($occupants.children().first(0).text()).toBe("oldnick"); - expect($chat_content.find('div.chat-info').length).toBe(2); + expect($chat_content.find('div.chat-info').length).toBe(1); expect($chat_content.find('div.chat-info:first').html()).toBe("oldnick has entered the room"); - expect($chat_content.find('div.chat-info:last').html()).toBe( - __(_converse.muc.new_nickname_messages["210"], "oldnick") - ); - presence = $pres().attrs({ + var presence = $pres().attrs({ from:'lounge@localhost/oldnick', id:'DC352437-C019-40EC-B590-AF29E879AF98', to:'dummy@localhost/pda', @@ -1660,13 +1661,13 @@ .c('status').attrs({code:'110'}).nodeTree; _converse.connection._dataRecv(test_utils.createRequest(presence)); - expect($chat_content.find('div.chat-info').length).toBe(3); + expect($chat_content.find('div.chat-info').length).toBe(2); expect($chat_content.find('div.chat-info').last().html()).toBe( __(_converse.muc.new_nickname_messages["303"], "newnick") ); $occupants = $(view.el.querySelector('.occupant-list')); - expect($occupants.children().length).toBe(0); + expect($occupants.children().length).toBe(1); presence = $pres().attrs({ from:'lounge@localhost/newnick', @@ -1682,12 +1683,10 @@ .c('status').attrs({code:'110'}).nodeTree; _converse.connection._dataRecv(test_utils.createRequest(presence)); - expect($chat_content.find('div.chat-info').length).toBe(4); - expect($chat_content.find('div.chat-info').get(2).textContent).toBe( + expect($chat_content.find('div.chat-info').length).toBe(2); + expect($chat_content.find('div.chat-info').get(1).textContent).toBe( __(_converse.muc.new_nickname_messages["303"], "newnick") ); - expect($chat_content.find('div.chat-info').last().html()).toBe( - "newnick has entered the room"); $occupants = $(view.el.querySelector('.occupant-list')); expect($occupants.children().length).toBe(1); expect($occupants.children().first(0).text()).toBe("newnick"); @@ -1907,9 +1906,9 @@ .up() .c('status').attrs({code:'110'}).up() .c('status').attrs({code:'307'}).nodeTree; + _converse.connection._dataRecv(test_utils.createRequest(presence)); var view = _converse.chatboxviews.get('lounge@localhost'); - view.onChatRoomPresence(presence); expect($(view.el.querySelector('.chat-area')).is(':visible')).toBeFalsy(); expect($(view.el.querySelector('.occupants')).is(':visible')).toBeFalsy(); var $chat_body = $(view.el.querySelector('.chatroom-body')); @@ -1992,16 +1991,12 @@ var view = _converse.chatboxviews.get('lounge@localhost'); spyOn(view, 'close').and.callThrough(); spyOn(_converse, 'emit'); - spyOn(view, 'leave'); + spyOn(view.model, 'leave'); view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called view.el.querySelector('.close-chatbox-button').click(); expect(view.close).toHaveBeenCalled(); - expect(view.leave).toHaveBeenCalled(); - // XXX: After refactoring, the chat box only gets closed - // once we have confirmation from the server. To test this, - // we would have to mock the returned presence stanza. - // See the "leave" method on the ChatRoomView. - // expect(_converse.emit).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object)); + expect(view.model.leave).toHaveBeenCalled(); + expect(_converse.emit).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object)); done(); })); }); @@ -2600,18 +2595,19 @@ test_utils.openChatRoomViaModal(_converse, 'problematic@muc.localhost', 'dummy') .then(function () { - var presence = $pres().attrs({ - from:'lounge@localhost/thirdwitch', - id:'n13mt3l', - to:'dummy@localhost/pda', - type:'error'}) - .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() - .c('error').attrs({by:'lounge@localhost', type:'auth'}) - .c('not-authorized').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; - var view = _converse.chatboxviews.get('problematic@muc.localhost'); spyOn(view, 'renderPasswordForm').and.callThrough(); - view.onChatRoomPresence(presence); + + var presence = $pres().attrs({ + from:'problematic@muc.localhost/dummy', + id:'n13mt3l', + to:'dummy@localhost/pda', + type:'error'}) + .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() + .c('error').attrs({by:'lounge@localhost', type:'auth'}) + .c('not-authorized').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}); + + _converse.connection._dataRecv(test_utils.createRequest(presence)); var $chat_body = $(view.el).find('.chatroom-body'); expect(view.renderPasswordForm).toHaveBeenCalled(); @@ -2636,16 +2632,16 @@ test_utils.openChatRoomViaModal(_converse, 'problematic@muc.localhost', 'dummy') .then(function () { var presence = $pres().attrs({ - from:'lounge@localhost/thirdwitch', - id:'n13mt3l', - to:'dummy@localhost/pda', - type:'error'}) + from:'problematic@muc.localhost/dummy', + id:'n13mt3l', + to:'dummy@localhost/pda', + type:'error'}) .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() .c('error').attrs({by:'lounge@localhost', type:'auth'}) .c('registration-required').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; var view = _converse.chatboxviews.get('problematic@muc.localhost'); spyOn(view, 'showErrorMessage').and.callThrough(); - view.onChatRoomPresence(presence); + _converse.connection._dataRecv(test_utils.createRequest(presence)); expect($(view.el).find('.chatroom-body p:last').text()).toBe('You are not on the member list of this room.'); done(); }); @@ -2659,16 +2655,16 @@ test_utils.openChatRoomViaModal(_converse, 'problematic@muc.localhost', 'dummy') .then(function () { var presence = $pres().attrs({ - from:'lounge@localhost/thirdwitch', - id:'n13mt3l', - to:'dummy@localhost/pda', - type:'error'}) + from:'problematic@muc.localhost/dummy', + id:'n13mt3l', + to:'dummy@localhost/pda', + type:'error'}) .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() .c('error').attrs({by:'lounge@localhost', type:'auth'}) .c('forbidden').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; var view = _converse.chatboxviews.get('problematic@muc.localhost'); spyOn(view, 'showErrorMessage').and.callThrough(); - view.onChatRoomPresence(presence); + _converse.connection._dataRecv(test_utils.createRequest(presence)); expect($(view.el).find('.chatroom-body p:last').text()).toBe('You have been banned from this room.'); done(); }); @@ -2682,16 +2678,16 @@ test_utils.openChatRoomViaModal(_converse, 'problematic@muc.localhost', 'dummy') .then(function () { var presence = $pres().attrs({ - from:'lounge@localhost/thirdwitch', - id:'n13mt3l', - to:'dummy@localhost/pda', - type:'error'}) + from:'problematic@muc.localhost/dummy', + id:'n13mt3l', + to:'dummy@localhost/pda', + type:'error'}) .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() .c('error').attrs({by:'lounge@localhost', type:'cancel'}) .c('conflict').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; var view = _converse.chatboxviews.get('problematic@muc.localhost'); spyOn(view, 'showErrorMessage').and.callThrough(); - view.onChatRoomPresence(presence); + _converse.connection._dataRecv(test_utils.createRequest(presence)); expect($(view.el).find('.chatroom-body form.chatroom-form label:first').text()).toBe('Please choose your nickname'); var $input = $(view.el).find('.chatroom-body form.chatroom-form input:first'); @@ -2722,14 +2718,14 @@ _converse.muc_nickname_from_jid = true; var attrs = { - from:'lounge@localhost/dummy', - id:'n13mt3l', + from:'problematic@muc.localhost/dummy', to:'dummy@localhost/pda', type:'error' }; + attrs.id = new Date().getTime(); var presence = $pres().attrs(attrs) .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() - .c('error').attrs({by:'lounge@localhost', type:'cancel'}) + .c('error').attrs({by:'problematic@muc.localhost', type:'cancel'}) .c('conflict').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; var view = _converse.chatboxviews.get('problematic@muc.localhost'); @@ -2738,24 +2734,26 @@ // Simulate repeatedly that there's already someone in the room // with that nickname - view.onChatRoomPresence(presence); + _converse.connection._dataRecv(test_utils.createRequest(presence)); expect(view.join).toHaveBeenCalledWith('dummy-2'); - attrs.from = 'lounge@localhost/dummy-2'; + attrs.from = 'problematic@muc.localhost/dummy-2'; + attrs.id = new Date().getTime(); presence = $pres().attrs(attrs) .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() - .c('error').attrs({by:'lounge@localhost', type:'cancel'}) + .c('error').attrs({by:'problematic@muc.localhost', type:'cancel'}) .c('conflict').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; - view.onChatRoomPresence(presence); + _converse.connection._dataRecv(test_utils.createRequest(presence)); expect(view.join).toHaveBeenCalledWith('dummy-3'); - attrs.from = 'lounge@localhost/dummy-3'; + attrs.from = 'problematic@muc.localhost/dummy-3'; + attrs.id = new Date().getTime(); presence = $pres().attrs(attrs) .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() - .c('error').attrs({by:'lounge@localhost', type:'cancel'}) + .c('error').attrs({by:'problematic@muc.localhost', type:'cancel'}) .c('conflict').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; - view.onChatRoomPresence(presence); + _converse.connection._dataRecv(test_utils.createRequest(presence)); expect(view.join).toHaveBeenCalledWith('dummy-4'); done(); }); @@ -2769,16 +2767,16 @@ test_utils.openChatRoomViaModal(_converse, 'problematic@muc.localhost', 'dummy') .then(function () { var presence = $pres().attrs({ - from:'lounge@localhost/thirdwitch', - id:'n13mt3l', - to:'dummy@localhost/pda', - type:'error'}) + from:'problematic@muc.localhost/dummy', + id:'n13mt3l', + to:'dummy@localhost/pda', + type:'error'}) .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() .c('error').attrs({by:'lounge@localhost', type:'cancel'}) .c('not-allowed').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; var view = _converse.chatboxviews.get('problematic@muc.localhost'); spyOn(view, 'showErrorMessage').and.callThrough(); - view.onChatRoomPresence(presence); + _converse.connection._dataRecv(test_utils.createRequest(presence)); expect($(view.el).find('.chatroom-body p:last').text()).toBe('You are not allowed to create new rooms.'); done(); }); @@ -2792,16 +2790,16 @@ test_utils.openChatRoomViaModal(_converse, 'problematic@muc.localhost', 'dummy') .then(function () { var presence = $pres().attrs({ - from:'lounge@localhost/thirdwitch', - id:'n13mt3l', - to:'dummy@localhost/pda', - type:'error'}) + from:'problematic@muc.localhost/dummy', + id:'n13mt3l', + to:'dummy@localhost/pda', + type:'error'}) .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() .c('error').attrs({by:'lounge@localhost', type:'cancel'}) .c('not-acceptable').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; var view = _converse.chatboxviews.get('problematic@muc.localhost'); spyOn(view, 'showErrorMessage').and.callThrough(); - view.onChatRoomPresence(presence); + _converse.connection._dataRecv(test_utils.createRequest(presence)); expect($(view.el).find('.chatroom-body p:last').text()).toBe("Your nickname doesn't conform to this room's policies."); done(); }); @@ -2815,16 +2813,16 @@ test_utils.openChatRoomViaModal(_converse, 'problematic@muc.localhost', 'dummy') .then(function () { var presence = $pres().attrs({ - from:'lounge@localhost/thirdwitch', - id:'n13mt3l', - to:'dummy@localhost/pda', - type:'error'}) + from:'problematic@muc.localhost/dummy', + id:'n13mt3l', + to:'dummy@localhost/pda', + type:'error'}) .c('x').attrs({xmlns:'http://jabber.org/protocol/muc'}).up() .c('error').attrs({by:'lounge@localhost', type:'cancel'}) .c('item-not-found').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; var view = _converse.chatboxviews.get('problematic@muc.localhost'); spyOn(view, 'showErrorMessage').and.callThrough(); - view.onChatRoomPresence(presence); + _converse.connection._dataRecv(test_utils.createRequest(presence)); expect($(view.el).find('.chatroom-body p:last').text()).toBe("This room does not (yet) exist."); done(); }); @@ -2838,7 +2836,7 @@ test_utils.openChatRoomViaModal(_converse, 'problematic@muc.localhost', 'dummy') .then(function () { var presence = $pres().attrs({ - from:'lounge@localhost/thirdwitch', + from:'problematic@muc.localhost/dummy', id:'n13mt3l', to:'dummy@localhost/pda', type:'error'}) @@ -2847,7 +2845,7 @@ .c('service-unavailable').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'}).nodeTree; var view = _converse.chatboxviews.get('problematic@muc.localhost'); spyOn(view, 'showErrorMessage').and.callThrough(); - view.onChatRoomPresence(presence); + _converse.connection._dataRecv(test_utils.createRequest(presence)); expect($(view.el).find('.chatroom-body p:last').text()).toBe("This room has reached its maximum number of occupants."); done(); }); @@ -3091,7 +3089,7 @@ test_utils.waitUntil(function () { return u.isVisible(modal.el); }, 1000).then(function () { - spyOn(_converse.ChatRoomView.prototype, 'getRoomFeatures').and.callFake(function () { + spyOn(_converse.ChatRoom.prototype, 'getRoomFeatures').and.callFake(function () { var deferred = new $.Deferred(); deferred.resolve(); return deferred.promise(); @@ -3126,7 +3124,7 @@ test_utils.waitUntil(function () { return u.isVisible(modal.el); }, 1000).then(function () { - spyOn(_converse.ChatRoomView.prototype, 'getRoomFeatures').and.callFake(function () { + spyOn(_converse.ChatRoom.prototype, 'getRoomFeatures').and.callFake(function () { var deferred = new $.Deferred(); deferred.resolve(); return deferred.promise(); @@ -3206,7 +3204,7 @@ type: 'groupchat' }).c('body').t(message).tree(); - view.handleMUCMessage(msg); + view.model.onMessage(msg); expect(roomspanel.el.querySelectorAll('.available-room').length).toBe(1); expect(roomspanel.el.querySelectorAll('.msgs-indicator').length).toBe(1); @@ -3218,7 +3216,7 @@ to: 'dummy@localhost', type: 'groupchat' }).c('body').t(message).tree(); - view.handleMUCMessage(msg); + view.model.onMessage(msg); expect(roomspanel.el.querySelectorAll('.available-room').length).toBe(1); expect(roomspanel.el.querySelectorAll('.msgs-indicator').length).toBe(1); @@ -3308,7 +3306,7 @@ to: 'dummy@localhost', type: 'groupchat' }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - view.handleMUCMessage(msg); + view.model.onMessage(msg); // Check that the notification appears inside the chatbox in the DOM var events = view.el.querySelectorAll('.chat-event'); @@ -3334,7 +3332,7 @@ to: 'dummy@localhost', type: 'groupchat' }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - view.handleMUCMessage(msg); + view.model.onMessage(msg); events = view.el.querySelectorAll('.chat-event'); expect(events.length).toBe(4); @@ -3356,7 +3354,7 @@ to: 'dummy@localhost', type: 'groupchat' }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - view.handleMUCMessage(msg); + view.model.onMessage(msg); events = view.el.querySelectorAll('.chat-event'); expect(events.length).toBe(4); expect(events[0].textContent).toEqual('some1 has entered the room'); @@ -3378,7 +3376,7 @@ to: 'dummy@localhost', type: 'groupchat' }).c('body').t('hello world').tree(); - view.handleMUCMessage(msg); + view.model.onMessage(msg); var messages = view.el.querySelectorAll('.message'); expect(messages.length).toBe(8); @@ -3483,7 +3481,7 @@ to: 'dummy@localhost', type: 'groupchat' }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - view.handleMUCMessage(msg); + view.model.onMessage(msg); // Check that the notification appears inside the chatbox in the DOM var events = view.el.querySelectorAll('.chat-event'); @@ -3503,7 +3501,7 @@ to: 'dummy@localhost', type: 'groupchat' }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - view.handleMUCMessage(msg); + view.model.onMessage(msg); events = view.el.querySelectorAll('.chat-event'); expect(events.length).toBe(3); @@ -3522,7 +3520,7 @@ to: 'dummy@localhost', type: 'groupchat' }).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - view.handleMUCMessage(msg); + view.model.onMessage(msg); events = view.el.querySelectorAll('.chat-event'); expect(events.length).toBe(3); expect(events[0].textContent).toEqual('some1 has entered the room'); @@ -3541,7 +3539,7 @@ to: 'dummy@localhost', type: 'groupchat' }).c('body').c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree(); - view.handleMUCMessage(msg); + view.model.onMessage(msg); events = view.el.querySelectorAll('.chat-event'); expect(events.length).toBe(3); expect(events[0].textContent).toEqual('some1 has entered the room'); diff --git a/spec/mam.js b/spec/mam.js index 5b1d2cb7d..e4219d0d4 100644 --- a/spec/mam.js +++ b/spec/mam.js @@ -39,7 +39,7 @@ `).firstElementChild; - chatroomview.onChatRoomMessage(stanza); + chatroomview.model.onMessage(stanza); expect(chatroomview.content.querySelectorAll('.chat-message').length).toBe(1); done(); }); diff --git a/spec/minchats.js b/spec/minchats.js index ce220d90d..d9bbca5f3 100644 --- a/spec/minchats.js +++ b/spec/minchats.js @@ -158,7 +158,7 @@ to: 'dummy@localhost', type: 'groupchat' }).c('body').t(message).tree(); - view.handleMUCMessage(msg); + view.model.onMessage(msg); expect($(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count')).is(':visible')).toBeTruthy(); expect($(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count')).text()).toBe('1'); diff --git a/spec/notification.js b/spec/notification.js index 3fdce2044..4b8b2316d 100644 --- a/spec/notification.js +++ b/spec/notification.js @@ -173,7 +173,7 @@ to: 'dummy@localhost', type: 'groupchat' }).c('body').t(text); - view.onChatRoomMessage(message.nodeTree); + view.model.onMessage(message.nodeTree); expect(_converse.playSoundNotification).toHaveBeenCalled(); text = "This message won't play a sound"; @@ -183,7 +183,7 @@ to: 'dummy@localhost', type: 'groupchat' }).c('body').t(text); - view.onChatRoomMessage(message.nodeTree); + view.model.onMessage(message.nodeTree); expect(_converse.playSoundNotification, 1); _converse.play_sounds = false; @@ -194,7 +194,7 @@ to: 'dummy@localhost', type: 'groupchat' }).c('body').t(text); - view.onChatRoomMessage(message.nodeTree); + view.model.onMessage(message.nodeTree); expect(_converse.playSoundNotification, 1); _converse.play_sounds = false; done(); diff --git a/spec/roomslist.js b/spec/roomslist.js index 50a970a9c..b92140f66 100644 --- a/spec/roomslist.js +++ b/spec/roomslist.js @@ -97,7 +97,7 @@ view.model.set({'minimized': true}); var contact_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@localhost'; var nick = mock.chatroom_names[0]; - view.handleMUCMessage( + view.model.onMessage( $msg({ from: room_jid+'/'+nick, id: (new Date()).getTime(), @@ -112,7 +112,7 @@ expect(_.includes(room_el.classList, 'unread-msgs')); // If the user is mentioned, the counter also gets updated - view.handleMUCMessage( + view.model.onMessage( $msg({ from: room_jid+'/'+nick, id: (new Date()).getTime(), @@ -123,7 +123,7 @@ var indicator_el = _converse.rooms_list_view.el.querySelector(".msgs-indicator"); expect(indicator_el.textContent).toBe('1'); - view.handleMUCMessage( + view.model.onMessage( $msg({ from: room_jid+'/'+nick, id: (new Date()).getTime(), diff --git a/src/converse-chatview.js b/src/converse-chatview.js index dec0826a5..450e10a97 100644 --- a/src/converse-chatview.js +++ b/src/converse-chatview.js @@ -416,6 +416,7 @@ 'isodate': isodate, 'data': data })); + this.insertDayIndicator(this.content.lastElementChild); this.scrollDown(); return isodate; }, diff --git a/src/converse-mam.js b/src/converse-mam.js index 5ff4784ff..7ddc56c1e 100644 --- a/src/converse-mam.js +++ b/src/converse-mam.js @@ -269,13 +269,16 @@ }, }, - ChatRoomView: { + ChatRoom: { - initialize () { - const { _converse } = this.__super__; - this.__super__.initialize.apply(this, arguments); - this.model.on('change:mam_enabled', this.fetchArchivedMessagesIfNecessary, this); - this.model.on('change:connection_status', this.fetchArchivedMessagesIfNecessary, this); + onMessage (stanza) { + /* MAM (message archive management XEP-0313) messages are + * ignored, since they're handled separately. + */ + if (sizzle(`[xmlns="${Strophe.NS.MAM}"]`, stanza).length > 0) { + return true; + } + return this.__super__.onMessage.apply(this, arguments); }, isDuplicate (message, original_stanza) { @@ -285,8 +288,18 @@ } const archive_id = getMessageArchiveID(original_stanza); if (archive_id) { - return this.model.messages.filter({'archive_id': archive_id}).length > 0; + return this.messages.filter({'archive_id': archive_id}).length > 0; } + } + }, + + ChatRoomView: { + + initialize () { + const { _converse } = this.__super__; + this.__super__.initialize.apply(this, arguments); + this.model.on('change:mam_enabled', this.fetchArchivedMessagesIfNecessary, this); + this.model.on('change:connection_status', this.fetchArchivedMessagesIfNecessary, this); }, renderChatArea () { @@ -297,16 +310,6 @@ return result; }, - handleMUCMessage (stanza) { - /* MAM (message archive management XEP-0313) messages are - * ignored, since they're handled separately. - */ - if (sizzle(`[xmlns="${Strophe.NS.MAM}"]`, stanza).length > 0) { - return true; - } - return this.__super__.handleMUCMessage.apply(this, arguments); - }, - fetchArchivedMessagesIfNecessary () { if (this.model.get('connection_status') !== converse.ROOMSTATUS.ENTERED || !this.model.get('mam_enabled') || @@ -321,7 +324,7 @@ fetchArchivedMessages (options) { /* Fetch archived chat messages for this Chat Room * - * Then, upon receiving them, call onChatRoomMessage + * Then, upon receiving them, call onMessage * so that they are displayed inside it. */ const that = this; @@ -337,7 +340,7 @@ function (messages) { that.clearSpinner(); if (messages.length) { - _.each(messages, that.onChatRoomMessage.bind(that)); + _.each(messages, that.model.onMessage.bind(that)); } }, function () { @@ -363,7 +366,6 @@ message_archiving_timeout: 8000, // Time (in milliseconds) to wait before aborting MAM request }); - _converse.onMAMError = function (iq) { if (iq.querySelectorAll('feature-not-implemented').length) { _converse.log( diff --git a/src/converse-muc-views.js b/src/converse-muc-views.js index 611fb9881..c7e18edc2 100644 --- a/src/converse-muc-views.js +++ b/src/converse-muc-views.js @@ -151,6 +151,99 @@ _converse.api.promises.add(['roomsPanelRendered']); + // Configuration values for this plugin + // ==================================== + // Refer to docs/source/configuration.rst for explanations of these + // configuration settings. + _converse.api.settings.update({ + auto_list_rooms: false, + hide_muc_server: false, // TODO: no longer implemented... + muc_disable_moderator_commands: false, + visible_toolbar_buttons: { + 'toggle_occupants': true + } + }); + + + 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; + } + + /* http://xmpp.org/extensions/xep-0045.html + * ---------------------------------------- + * 100 message Entering a room 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 room + * 102 message Configuration change Inform occupants that room now shows unavailable members + * 103 message Configuration change Inform occupants that room now does not show unavailable members + * 104 message Configuration change Inform occupants that a non-privacy-related room configuration change has occurred + * 110 presence Any room presence Inform user that presence refers to one of its own room occupants + * 170 message or initial presence Configuration change Inform occupants that room logging is now enabled + * 171 message Configuration change Inform occupants that room logging is now disabled + * 172 message Configuration change Inform occupants that the room is now non-anonymous + * 173 message Configuration change Inform occupants that the room is now semi-anonymous + * 174 message Configuration change Inform occupants that the room is now fully-anonymous + * 201 presence Entering a room Inform user that a new room has been created + * 210 presence Entering a room Inform user that the service has assigned or modified the occupant's roomnick + * 301 presence Removal from room Inform user that he or she has been banned from the room + * 303 presence Exiting a room Inform all occupants of new room nickname + * 307 presence Removal from room Inform user that he or she has been kicked from the room + * 321 presence Removal from room Inform user that he or she is being removed from the room because of an affiliation change + * 322 presence Removal from room Inform user that he or she is being removed from the room because the room has been changed to members-only and the user is not a member + * 332 presence Removal from room Inform user that he or she is being removed from the room because of a system shutdown + */ + _converse.muc = { + info_messages: { + 100: __('This room is not anonymous'), + 102: __('This room now shows unavailable members'), + 103: __('This room does not show unavailable members'), + 104: __('The room configuration has changed'), + 170: __('Room logging is now enabled'), + 171: __('Room logging is now disabled'), + 172: __('This room is now no longer anonymous'), + 173: __('This room is now semi-anonymous'), + 174: __('This room is now fully-anonymous'), + 201: __('A new room has been created') + }, + + disconnect_messages: { + 301: __('You have been banned from this room'), + 307: __('You have been kicked from this room'), + 321: __("You have been removed from this room because of an affiliation change"), + 322: __("You have been removed from this room because the room has changed to members-only and you're not a member"), + 332: __("You have been removed from this room because the MUC (Multi-user chat) service 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') + } + }; + function insertRoomInfo (el, stanza) { /* Insert room info (based on returned #disco IQ stanza) @@ -422,13 +515,18 @@ this.markScrolled = _.debounce(this._markScrolled, 100); this.model.messages.on('add', this.onMessageAdded, this); - this.model.on('show', this.show, this); - this.model.on('destroy', this.hide, this); - this.model.on('change:connection_status', this.afterConnected, this); this.model.on('change:affiliation', this.renderHeading, this); this.model.on('change:chat_state', this.sendChatState, this); + this.model.on('change:connection_status', this.afterConnected, this); this.model.on('change:description', this.renderHeading, this); this.model.on('change:name', this.renderHeading, this); + this.model.on('change:subject', this.setChatRoomSubject, this); + this.model.on('configurationNeeded', this.getAndRenderConfigurationForm, this); + this.model.on('destroy', this.hide, this); + this.model.on('show', this.show, this); + + this.model.occupants.on('add', this.showJoinNotification, this); + this.model.occupants.on('remove', this.showLeaveNotification, this); this.createEmojiPicker(); this.createOccupantsView(); @@ -441,7 +539,7 @@ this.fetchMessages(); _converse.emit('chatRoomOpened', this); } - this.getRoomFeatures().then(handler, handler); + this.model.getRoomFeatures().then(handler, handler); } else { this.fetchMessages(); _converse.emit('chatRoomOpened', this); @@ -487,9 +585,8 @@ createOccupantsView () { /* Create the ChatRoomOccupantsView Backbone.NativeView */ - const model = new _converse.ChatRoomOccupants(); - model.chatroomview = this; - this.occupantsview = new _converse.ChatRoomOccupantsView({'model': model}); + this.model.occupants.chatroomview = this; + this.occupantsview = new _converse.ChatRoomOccupantsView({'model': this.model.occupants}); this.occupantsview.model.on('change:role', this.informOfOccupantsRoleChange, this); return this; }, @@ -550,6 +647,7 @@ afterConnected () { if (this.model.get('connection_status') === converse.ROOMSTATUS.ENTERED) { + this.hideSpinner(); this.setChatState(_converse.ACTIVE); this.scrollDown(); this.focus(); @@ -583,7 +681,12 @@ /* Close this chat box, which implies leaving the room as * well. */ - this.leave(); + this.hide(); + if (Backbone.history.getFragment() === "converse/room?jid="+this.model.get('jid')) { + _converse.router.navigate(''); + } + this.model.leave(); + _converse.ChatBoxView.prototype.close.apply(this, arguments); }, setOccupantsVisibility () { @@ -802,7 +905,7 @@ case 'nick': _converse.connection.send($pres({ from: _converse.connection.jid, - to: this.getRoomJIDAndNick(match[2]), + to: this.model.getRoomJIDAndNick(match[2]), id: _converse.connection.getUniqueId() }).tree()); break; @@ -848,76 +951,39 @@ } }, - handleMUCMessage (stanza) { - /* Handler for all MUC messages sent to this chat room. - * - * Parameters: - * (XMLElement) stanza: The message stanza. - */ - const configuration_changed = stanza.querySelector("status[code='104']"); - const logging_enabled = stanza.querySelector("status[code='170']"); - const logging_disabled = stanza.querySelector("status[code='171']"); - const room_no_longer_anon = stanza.querySelector("status[code='172']"); - const room_now_semi_anon = stanza.querySelector("status[code='173']"); - const room_now_fully_anon = stanza.querySelector("status[code='173']"); - if (configuration_changed || logging_enabled || logging_disabled || - room_no_longer_anon || room_now_semi_anon || room_now_fully_anon) { - this.getRoomFeatures(); - } - _.flow(this.showStatusMessages.bind(this), this.onChatRoomMessage.bind(this))(stanza); - return true; - }, - - getRoomJIDAndNick (nick) { - /* Utility method to construct the JID for the current user - * as occupant of the room. - * - * This is the room JID, with the user's nick added at the - * end. - * - * For example: room@conference.example.org/nickname - */ - if (nick) { - this.model.save({'nick': nick}); - } else { - nick = this.model.get('nick'); - } - const room = this.model.get('jid'); - const jid = Strophe.getBareJidFromJid(room); - return jid + (nick !== null ? `/${nick}` : ""); - }, - registerHandlers () { /* Register presence and message handlers for this chat * room */ - const room_jid = this.model.get('jid'); - this.removeHandlers(); - this.presence_handler = _converse.connection.addHandler( - this.onChatRoomPresence.bind(this), - Strophe.NS.MUC, 'presence', null, null, room_jid, - {'ignoreNamespaceFragment': true, 'matchBareFromJid': true} - ); - this.message_handler = _converse.connection.addHandler( - this.handleMUCMessage.bind(this), - null, 'message', 'groupchat', null, room_jid, - {'matchBareFromJid': true} - ); + // 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)); }, - removeHandlers () { - /* Remove the presence and message handlers that were - * registered for this chat room. + onPresence (pres) { + /* Handles all MUC presence stanzas. + * + * Parameters: + * (XMLElement) pres: The stanza */ - if (this.message_handler) { - _converse.connection.deleteHandler(this.message_handler); - delete this.message_handler; + // 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); } - if (this.presence_handler) { - _converse.connection.deleteHandler(this.presence_handler); - delete this.presence_handler; - } - return this; }, join (nick, password) { @@ -928,66 +994,14 @@ * (String) password: Optional password, if required by * the room. */ - nick = nick ? nick : this.model.get('nick'); - if (!nick) { + if (!nick && !this.model.get('nick')) { this.checkForReservedNick(); return this; } - if (this.model.get('connection_status') === converse.ROOMSTATUS.ENTERED) { - // We have restored a chat room from session storage, - // so we don't send out a presence stanza again. - return this; - } - - const stanza = $pres({ - 'from': _converse.connection.jid, - 'to': this.getRoomJIDAndNick(nick) - }).c("x", {'xmlns': Strophe.NS.MUC}) - .c("history", {'maxstanzas': _converse.muc_history_max_stanzas}).up(); - if (password) { - stanza.cnode(Strophe.xmlElement("password", [], password)); - } - this.model.save('connection_status', converse.ROOMSTATUS.CONNECTING); - _converse.connection.send(stanza); + this.model.join(nick, password); return this; }, - sendUnavailablePresence (exit_msg) { - const presence = $pres({ - type: "unavailable", - from: _converse.connection.jid, - to: this.getRoomJIDAndNick() - }); - if (exit_msg !== null) { - presence.c("status", exit_msg); - } - _converse.connection.sendPresence(presence); - }, - - leave(exit_msg) { - /* Leave the chat room. - * - * Parameters: - * (String) exit_msg: Optional message to indicate your - * reason for leaving. - */ - this.hide(); - if (Backbone.history.getFragment() === "converse/room?jid="+this.model.get('jid')) { - _converse.router.navigate(''); - } - this.occupantsview.model.reset(); - this.occupantsview.model.browserStorage._clear(); - if (_converse.connection.connected) { - this.sendUnavailablePresence(exit_msg); - } - u.safeSave( - this.model, - {'connection_status': converse.ROOMSTATUS.DISCONNECTED} - ); - this.removeHandlers(); - _converse.ChatBoxView.prototype.close.apply(this, arguments); - }, - renderConfigurationForm (stanza) { /* Renders a form given an IQ stanza containing the current * room configuration. @@ -1037,78 +1051,15 @@ form_el.addEventListener('submit', (ev) => { ev.preventDefault(); - this.saveConfiguration(ev.target).then( - this.getRoomFeatures.bind(this) + this.model.saveConfiguration(ev.target).then( + this.model.getRoomFeatures.bind(this.model) ); + this.closeForm(); }, false ); }, - saveConfiguration (form) { - /* Submit the room configuration form by sending an IQ - * stanza to the server. - * - * Returns a promise which resolves once the XMPP server - * has return a response IQ. - * - * Parameters: - * (HTMLElement) form: The configuration form DOM element. - */ - return new Promise((resolve, reject) => { - const inputs = form ? sizzle(':input:not([type=button]):not([type=submit])', form) : [], - configArray = _.map(inputs, u.webForm2xForm); - this.model.sendConfiguration(configArray, resolve, reject); - this.closeForm(); - }); - }, - - autoConfigureChatRoom () { - /* Automatically configure room based on the - * 'roomconfig' data on this view's model. - * - * Returns a promise which resolves once a response IQ has - * been received. - * - * Parameters: - * (XMLElement) stanza: IQ stanza from the server, - * containing the configuration. - */ - const that = this; - return new Promise((resolve, reject) => { - this.fetchRoomConfiguration().then(function (stanza) { - const configArray = [], - fields = stanza.querySelectorAll('field'), - config = that.model.get('roomconfig'); - let count = fields.length; - - _.each(fields, function (field) { - const fieldname = field.getAttribute('var').replace('muc#roomconfig_', ''), - type = field.getAttribute('type'); - let value; - if (fieldname in config) { - switch (type) { - case 'boolean': - value = config[fieldname] ? 1 : 0; - break; - case 'list-multi': - // TODO: we don't yet handle "list-multi" types - value = field.innerHTML; - break; - default: - value = config[fieldname]; - } - field.innerHTML = $build('value').t(value); - } - configArray.push(field); - if (!--count) { - that.model.sendConfiguration(configArray, resolve, reject); - } - }); - }); - }); - }, - closeForm () { /* Remove the configuration form without submitting and * return to the chat view. @@ -1117,47 +1068,6 @@ this.renderAfterTransition(); }, - fetchRoomConfiguration (handler) { - /* Send an IQ stanza to fetch the room configuration data. - * Returns a promise which resolves once the response IQ - * has been received. - * - * Parameters: - * (Function) handler: The handler for the response IQ - */ - return new Promise((resolve, reject) => { - _converse.connection.sendIQ( - $iq({ - 'to': this.model.get('jid'), - 'type': "get" - }).c("query", {xmlns: Strophe.NS.MUC_OWNER}), - (iq) => { - if (handler) { - handler.apply(this, arguments); - } - resolve(iq); - }, - reject // errback - ); - }); - }, - - - getRoomFeatures () { - /* Fetch the room disco info, parse it and then - * save it on the Backbone.Model of this chat rooms. - */ - return new Promise((resolve, reject) => { - _converse.connection.disco.info( - this.model.get('jid'), - null, - _.flow(this.model.parseRoomFeatures.bind(this.model), resolve), - () => { reject(new Error("Could not parse the room features")) }, - 5000 - ); - }); - }, - getAndRenderConfigurationForm (ev) { /* Start the process of configuring a chat room, either by * rendering a configuration form, or by auto-configuring @@ -1174,7 +1084,7 @@ * the settings. */ this.showSpinner(); - this.fetchRoomConfiguration() + this.model.fetchRoomConfiguration() .then(this.renderConfigurationForm.bind(this)) .catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR)); }, @@ -1207,7 +1117,6 @@ this.onNickNameFound.bind(this), this.onNickNameNotFound.bind(this) ) - return this; }, onNickNameFound (iq) { @@ -1410,7 +1319,7 @@ return notification; }, - displayNotificationsforUser (notification) { + showNotificationsforUser (notification) { /* Given the notification object generated by * parseXUserElement, display any relevant messages and * information to the user. @@ -1444,13 +1353,16 @@ } }, - displayJoinNotification (stanza) { - const nick = Strophe.getResourceFromJid(stanza.getAttribute('from')); - const stat = stanza.querySelector('status'); + showJoinNotification (occupant) { + if (this.model.get('connection_status') !== converse.ROOMSTATUS.ENTERED) { + return; + } + const nick = occupant.get('nick'); + const stat = occupant.get('status'); const last_el = this.content.lastElementChild; if (_.includes(_.get(last_el, 'classList', []), 'chat-info') && - _.get(last_el, 'dataset', {}).leave === `"${nick}"`) { + _.get(last_el, 'dataset', {}).leave === `"${nick}"`) { last_el.outerHTML = tpl_info({ 'data': `data-leavejoin="${nick}"`, @@ -1460,10 +1372,10 @@ }); } else { let message; - if (_.get(stat, 'textContent')) { - message = __('%1$s has entered the room. "%2$s"', nick, stat.textContent); - } else { + if (_.isNil(stat)) { message = __('%1$s has entered the room', nick); + } else { + message = __('%1$s has entered the room. "%2$s"', nick, stat); } const data = { 'data': `data-join="${nick}"`, @@ -1484,18 +1396,18 @@ this.scrollDown(); }, - displayLeaveNotification (stanza) { - const nick = Strophe.getResourceFromJid(stanza.getAttribute('from')); - const stat = stanza.querySelector('status'); + showLeaveNotification (occupant) { + const nick = occupant.get('nick'); + const stat = occupant.get('status'); const last_el = this.content.lastElementChild; if (_.includes(_.get(last_el, 'classList', []), 'chat-info') && _.get(last_el, 'dataset', {}).join === `"${nick}"`) { let message; - if (_.get(stat, 'textContent')) { - message = __('%1$s has entered and left the room. "%2$s"', nick, stat.textContent); - } else { + if (_.isNil(stat)) { message = __('%1$s has entered and left the room', nick); + } else { + message = __('%1$s has entered and left the room. "%2$s"', nick, stat); } last_el.outerHTML = tpl_info({ @@ -1506,10 +1418,10 @@ }); } else { let message; - if (_.get(stat, 'textContent')) { - message = __('%1$s has left the room. "%2$s"', nick, stat.textContent); - } else { + if (_.isNil(stat)) { message = __('%1$s has left the room', nick); + } else { + message = __('%1$s has left the room. "%2$s"', nick, stat); } const data = { 'message': message, @@ -1530,20 +1442,6 @@ this.scrollDown(); }, - displayJoinOrLeaveNotification (stanza) { - if (stanza.getAttribute('type') === 'unavailable') { - this.displayLeaveNotification(stanza); - } else { - const nick = Strophe.getResourceFromJid(stanza.getAttribute('from')); - if (!this.occupantsview.model.find({'nick': nick})) { - // Only show join message if we don't already have the - // occupant model. Doing so avoids showing duplicate - // join messages. - this.displayJoinNotification(stanza); - } - } - }, - showStatusMessages (stanza) { /* Check for status codes and communicate their purpose to the user. * See: http://xmpp.org/registrar/mucstatus.html @@ -1556,16 +1454,7 @@ 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); - if (_.isEmpty(notifications)) { - if (_converse.muc_show_join_leave && - stanza.nodeName === 'presence' && - this.model.get('connection_status') === converse.ROOMSTATUS.ENTERED) { - this.displayJoinOrLeaveNotification(stanza); - } - } else { - _.each(notifications, this.displayNotificationsforUser.bind(this)); - } - return stanza; + _.each(notifications, this.showNotificationsforUser.bind(this)); }, showErrorMessageFromPresence (presence) { @@ -1637,87 +1526,18 @@ return this; }, - onOwnChatRoomPresence (pres) { - /* Handles a received presence relating to the current - * user. - * - * For locked rooms (which are by definition "new"), the - * room will either be auto-configured or created instantly - * (with default config) or a configuration room will be - * rendered. - * - * If the room is not locked, then the room will be - * auto-configured only if applicable and if the current - * user is the room's owner. - * - * Parameters: - * (XMLElement) pres: The stanza - */ - this.model.saveAffiliationAndRole(pres); - - const locked_room = pres.querySelector("status[code='201']"); - if (locked_room) { - if (this.model.get('auto_configure')) { - this.autoConfigureChatRoom().then(this.getRoomFeatures.bind(this)); - } else if (_converse.muc_instant_rooms) { - // Accept default configuration - this.saveConfiguration().then(this.getRoomFeatures.bind(this)); - } else { - this.getAndRenderConfigurationForm(); - return; // We haven't yet entered the room, so bail here. - } - } else if (!this.model.get('features_fetched')) { - // The features for this room weren't fetched. - // That must mean it's a new room without locking - // (in which case Prosody doesn't send a 201 status), - // otherwise the features would have been fetched in - // the "initialize" method already. - if (this.model.get('affiliation') === 'owner' && this.model.get('auto_configure')) { - this.autoConfigureChatRoom().then(this.getRoomFeatures.bind(this)); - } else { - this.getRoomFeatures(); - } - } - this.model.save('connection_status', converse.ROOMSTATUS.ENTERED); - }, - - onChatRoomPresence (pres) { - /* Handles all MUC presence stanzas. - * - * Parameters: - * (XMLElement) pres: The stanza - */ - if (pres.getAttribute('type') === 'error') { - this.model.save('connection_status', converse.ROOMSTATUS.DISCONNECTED); - this.showErrorMessageFromPresence(pres); - return true; - } - const is_self = pres.querySelector("status[code='110']"); - if (is_self && pres.getAttribute('type') !== 'unavailable') { - this.onOwnChatRoomPresence(pres); - } - this.hideSpinner().showStatusMessages(pres); - // This must be called after showStatusMessages so that - // "join" messages are correctly shown. - this.occupantsview.updateOccupantsOnPresence(pres); - if (this.model.get('role') !== 'none' && - this.model.get('connection_status') === converse.ROOMSTATUS.CONNECTING) { - this.model.save('connection_status', converse.ROOMSTATUS.CONNECTED); - } - return true; - }, - - setChatRoomSubject (sender, subject) { + setChatRoomSubject () { // For translators: the %1$s and %2$s parts will get // replaced by the user and topic text respectively // Example: Topic set by JC Brand to: Hello World! + const subject = this.model.get('subject'); this.content.insertAdjacentHTML( 'beforeend', tpl_info({ 'data': '', 'isodate': moment().format(), 'extra_classes': 'chat-event', - 'message': __('Topic set by %1$s', sender) + 'message': __('Topic set by %1$s', subject.author) })); this.content.insertAdjacentHTML( 'beforeend', @@ -1725,91 +1545,9 @@ 'data': '', 'isodate': moment().format(), 'extra_classes': 'chat-topic', - 'message': subject + 'message': subject.text })); this.scrollDown(); - }, - - isDuplicateBasedOnTime (message) { - /* Checks whether a received messages is actually a - * duplicate based on whether it has a "ts" attribute - * with a unix timestamp. - * - * This is used for better integration with Slack's XMPP - * gateway, which doesn't use message IDs but instead the - * aforementioned "ts" attributes. - */ - const entity = _converse.disco_entities.get(_converse.domain); - if (entity.identities.where({'name': "Slack-XMPP"})) { - const ts = message.getAttribute('ts'); - if (_.isNull(ts)) { - return false; - } else { - return this.model.messages.where({ - 'sender': 'me', - 'message': this.model.getMessageBody(message) - }).filter( - (msg) => Math.abs(moment(msg.get('time')).diff(moment.unix(ts))) < 5000 - ).length > 0; - } - } - return false; - }, - - isDuplicate (message, original_stanza) { - const msgid = message.getAttribute('id'), - jid = message.getAttribute('from'), - resource = Strophe.getResourceFromJid(jid), - sender = resource && Strophe.unescapeNode(resource) || ''; - if (msgid) { - return this.model.messages.filter( - // Some bots (like HAL in the prosody chatroom) - // respond to commands with the same ID as the - // original message. So we also check the sender. - (msg) => msg.get('msgid') === msgid && msg.get('fullname') === sender - ).length > 0; - } - return this.isDuplicateBasedOnTime(message); - }, - - onChatRoomMessage (message) { - /* Given a stanza, create a message - * Backbone.Model if appropriate. - * - * Parameters: - * (XMLElement) msg: The received message stanza - */ - const original_stanza = message, - forwarded = message.querySelector('forwarded'); - let delay; - if (!_.isNull(forwarded)) { - message = forwarded.querySelector('message'); - delay = forwarded.querySelector('delay'); - } - const jid = message.getAttribute('from'), - resource = Strophe.getResourceFromJid(jid), - sender = resource && Strophe.unescapeNode(resource) || '', - subject = _.propertyOf(message.querySelector('subject'))('textContent'); - - if (this.isDuplicate(message, original_stanza)) { - return true; - } - if (subject) { - this.setChatRoomSubject(sender, subject); - } - if (sender === '') { - return true; - } - this.model.incrementUnreadMsgCounter(original_stanza); - this.model.createMessage(message, delay, original_stanza); - if (sender !== this.model.get('nick')) { - // We only emit an event if it's not our own message - _converse.emit( - 'message', - {'stanza': original_stanza, 'chatbox': this.model} - ); - } - return true; } }); @@ -2030,86 +1768,6 @@ `height: calc(100% - ${el.offsetHeight}px - 5em);`; }, - parsePresence (pres) { - const id = Strophe.getResourceFromJid(pres.getAttribute("from")); - const data = { - nick: id, - type: pres.getAttribute("type"), - states: [] - }; - _.each(pres.childNodes, function (child) { - switch (child.nodeName) { - case "status": - data.status = child.textContent || null; - break; - case "show": - data.show = child.textContent || 'online'; - break; - case "x": - if (child.getAttribute("xmlns") === Strophe.NS.MUC_USER) { - _.each(child.childNodes, function (item) { - switch (item.nodeName) { - case "item": - data.affiliation = item.getAttribute("affiliation"); - data.role = item.getAttribute("role"); - data.jid = item.getAttribute("jid"); - data.nick = item.getAttribute("nick") || data.nick; - break; - case "status": - if (item.getAttribute("code")) { - data.states.push(item.getAttribute("code")); - } - } - }); - } - } - }); - return data; - }, - - findOccupant (data) { - /* Try to find an existing occupant based on the passed in - * data object. - * - * If we have a JID, we use that as lookup variable, - * otherwise we use the nick. We don't always have both, - * but should have at least one or the other. - */ - const jid = Strophe.getBareJidFromJid(data.jid); - if (jid !== null) { - return this.model.where({'jid': jid}).pop(); - } else { - return this.model.where({'nick': data.nick}).pop(); - } - }, - - updateOccupantsOnPresence (pres) { - /* Given a presence stanza, update the occupant models - * based on its contents. - * - * Parameters: - * (XMLElement) pres: The presence stanza - */ - const data = this.parsePresence(pres); - if (data.type === 'error') { - return true; - } - const occupant = this.findOccupant(data); - if (data.type === 'unavailable') { - if (occupant) { occupant.destroy(); } - } else { - const jid = Strophe.getBareJidFromJid(data.jid); - const attributes = _.extend(data, { - 'jid': jid ? jid : undefined, - 'resource': data.jid ? Strophe.getResourceFromJid(data.jid) : undefined - }); - if (occupant) { - occupant.save(attributes); - } else { - this.model.create(attributes); - } - } - }, promptForInvite (suggestion) { const reason = prompt( diff --git a/src/converse-muc.js b/src/converse-muc.js index 0265b1770..96a9788bc 100644 --- a/src/converse-muc.js +++ b/src/converse-muc.js @@ -39,6 +39,8 @@ Strophe.addNamespace('MUC_ROOMCONF', Strophe.NS.MUC + "#roomconfig"); Strophe.addNamespace('MUC_USER', Strophe.NS.MUC + "#user"); + converse.MUC_NICK_CHANGED_CODE = "303"; + converse.CHATROOMS_TYPE = 'chatroom'; converse.ROOM_FEATURES = [ @@ -107,90 +109,6 @@ const { _converse } = this, { __ } = _converse; - 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; - } - - // XXX: Inside plugins, all calls to the translation machinery - // (e.g. u.__) should only be done in the initialize function. - // If called before, we won't know what language the user wants, - // and it'll fall back to English. - - /* http://xmpp.org/extensions/xep-0045.html - * ---------------------------------------- - * 100 message Entering a room 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 room - * 102 message Configuration change Inform occupants that room now shows unavailable members - * 103 message Configuration change Inform occupants that room now does not show unavailable members - * 104 message Configuration change Inform occupants that a non-privacy-related room configuration change has occurred - * 110 presence Any room presence Inform user that presence refers to one of its own room occupants - * 170 message or initial presence Configuration change Inform occupants that room logging is now enabled - * 171 message Configuration change Inform occupants that room logging is now disabled - * 172 message Configuration change Inform occupants that the room is now non-anonymous - * 173 message Configuration change Inform occupants that the room is now semi-anonymous - * 174 message Configuration change Inform occupants that the room is now fully-anonymous - * 201 presence Entering a room Inform user that a new room has been created - * 210 presence Entering a room Inform user that the service has assigned or modified the occupant's roomnick - * 301 presence Removal from room Inform user that he or she has been banned from the room - * 303 presence Exiting a room Inform all occupants of new room nickname - * 307 presence Removal from room Inform user that he or she has been kicked from the room - * 321 presence Removal from room Inform user that he or she is being removed from the room because of an affiliation change - * 322 presence Removal from room Inform user that he or she is being removed from the room because the room has been changed to members-only and the user is not a member - * 332 presence Removal from room Inform user that he or she is being removed from the room because of a system shutdown - */ - _converse.muc = { - info_messages: { - 100: __('This room is not anonymous'), - 102: __('This room now shows unavailable members'), - 103: __('This room does not show unavailable members'), - 104: __('The room configuration has changed'), - 170: __('Room logging is now enabled'), - 171: __('Room logging is now disabled'), - 172: __('This room is now no longer anonymous'), - 173: __('This room is now semi-anonymous'), - 174: __('This room is now fully-anonymous'), - 201: __('A new room has been created') - }, - - disconnect_messages: { - 301: __('You have been banned from this room'), - 307: __('You have been kicked from this room'), - 321: __("You have been removed from this room because of an affiliation change"), - 322: __("You have been removed from this room because the room has changed to members-only and you're not a member"), - 332: __("You have been removed from this room because the MUC (Multi-user chat) service 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') - } - }; - // Configuration values for this plugin // ==================================== // Refer to docs/source/configuration.rst for explanations of these @@ -200,17 +118,10 @@ allow_muc_invitations: true, auto_join_on_invite: false, auto_join_rooms: [], - auto_list_rooms: false, - hide_muc_server: false, - muc_disable_moderator_commands: false, muc_domain: undefined, muc_history_max_stanzas: undefined, muc_instant_rooms: true, - muc_nickname_from_jid: false, - muc_show_join_leave: true, - visible_toolbar_buttons: { - 'toggle_occupants': true - }, + muc_nickname_from_jid: false }); _converse.api.promises.add(['roomsAutoJoined']); @@ -275,6 +186,156 @@ ); }, + initialize() { + this.constructor.__super__.initialize.apply(this, arguments); + this.occupants = new _converse.ChatRoomOccupants(); + this.registerHandlers(); + }, + + registerHandlers () { + /* Register presence and message handlers for this chat + * room + */ + const room_jid = this.get('jid'); + this.removeHandlers(); + this.presence_handler = _converse.connection.addHandler((stanza) => { + _.each(_.values(this.handlers.presence), (callback) => callback(stanza)); + this.onPresence(stanza); + return true; + }, + Strophe.NS.MUC, 'presence', null, null, room_jid, + {'ignoreNamespaceFragment': true, 'matchBareFromJid': true} + ); + this.message_handler = _converse.connection.addHandler((stanza) => { + _.each(_.values(this.handlers.message), (callback) => callback(stanza)); + this.onMessage(stanza); + return true; + }, null, 'message', 'groupchat', null, room_jid, + {'matchBareFromJid': true} + ); + }, + + removeHandlers () { + /* Remove the presence and message handlers that were + * registered for this chat room. + */ + if (this.message_handler) { + _converse.connection.deleteHandler(this.message_handler); + delete this.message_handler; + } + if (this.presence_handler) { + _converse.connection.deleteHandler(this.presence_handler); + delete this.presence_handler; + } + 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; + }, + + join (nick, password) { + /* Join the chat room. + * + * Parameters: + * (String) nick: The user's nickname + * (String) password: Optional password, if required by + * the room. + */ + nick = nick ? nick : this.get('nick'); + if (!nick) { + throw new TypeError('join: You need to provide a valid nickname'); + } + if (this.get('connection_status') === converse.ROOMSTATUS.ENTERED) { + // We have restored a chat room from session storage, + // so we don't send out a presence stanza again. + return this; + } + const stanza = $pres({ + 'from': _converse.connection.jid, + 'to': this.getRoomJIDAndNick(nick) + }).c("x", {'xmlns': Strophe.NS.MUC}) + .c("history", {'maxstanzas': _converse.muc_history_max_stanzas}).up(); + if (password) { + stanza.cnode(Strophe.xmlElement("password", [], password)); + } + this.save('connection_status', converse.ROOMSTATUS.CONNECTING); + _converse.connection.send(stanza); + return this; + }, + + leave (exit_msg) { + /* Leave the chat room. + * + * Parameters: + * (String) exit_msg: Optional message to indicate your + * reason for leaving. + */ + this.occupants.reset(); + this.occupants.browserStorage._clear(); + if (_converse.connection.connected) { + this.sendUnavailablePresence(exit_msg); + } + u.safeSave(this, {'connection_status': converse.ROOMSTATUS.DISCONNECTED}); + this.removeHandlers(); + }, + + sendUnavailablePresence (exit_msg) { + const presence = $pres({ + type: "unavailable", + from: _converse.connection.jid, + to: this.getRoomJIDAndNick() + }); + if (exit_msg !== null) { + presence.c("status", exit_msg); + } + _converse.connection.sendPresence(presence); + }, + + getRoomFeatures () { + /* Fetch the room disco info, parse it and then save it. + */ + return new Promise((resolve, reject) => { + _converse.connection.disco.info( + this.get('jid'), + null, + _.flow(this.parseRoomFeatures.bind(this), resolve), + () => { reject(new Error("Could not parse the room features")) }, + 5000 + ); + }); + }, + + getRoomJIDAndNick (nick) { + /* Utility method to construct the JID for the current user + * as occupant of the room. + * + * This is the room JID, with the user's nick added at the + * end. + * + * For example: room@conference.example.org/nickname + */ + if (nick) { + this.save({'nick': nick}); + } else { + nick = this.get('nick'); + } + const room = this.get('jid'); + const jid = Strophe.getBareJidFromJid(room); + return jid + (nick !== null ? `/${nick}` : ""); + }, + directInvite (recipient, reason) { /* Send a direct invitation as per XEP-0249 * @@ -314,30 +375,6 @@ }); }, - - sendConfiguration (config, callback, errback) { - /* Send an IQ stanza with the room configuration. - * - * Parameters: - * (Array) config: The room configuration - * (Function) callback: Callback upon succesful IQ response - * The first parameter passed in is IQ containing the - * room configuration. - * The second is the response IQ from the server. - * (Function) errback: Callback upon error IQ response - * The first parameter passed in is IQ containing the - * room configuration. - * The second is the response IQ from the server. - */ - const iq = $iq({to: this.get('jid'), type: "set"}) - .c("query", {xmlns: Strophe.NS.MUC_OWNER}) - .c("x", {xmlns: Strophe.NS.XFORM, type: "submit"}); - _.each(config || [], function (node) { iq.cnode(node).up(); }); - callback = _.isUndefined(callback) ? _.noop : _.partial(callback, iq.nodeTree); - errback = _.isUndefined(errback) ? _.noop : _.partial(errback, iq.nodeTree); - return _converse.connection.sendIQ(iq, callback, errback); - }, - parseRoomFeatures (iq) { /* Parses an IQ stanza containing the room's features. * @@ -432,6 +469,106 @@ return Promise.all(promises); }, + saveConfiguration (form) { + /* Submit the room configuration form by sending an IQ + * stanza to the server. + * + * Returns a promise which resolves once the XMPP server + * has return a response IQ. + * + * Parameters: + * (HTMLElement) form: The configuration form DOM element. + * If no form is provided, the default configuration + * values will be used. + */ + return new Promise((resolve, reject) => { + const inputs = form ? sizzle(':input:not([type=button]):not([type=submit])', form) : [], + configArray = _.map(inputs, u.webForm2xForm); + this.sendConfiguration(configArray, resolve, reject); + }); + }, + + autoConfigureChatRoom () { + /* Automatically configure room based on this model's + * 'roomconfig' data. + * + * Returns a promise which resolves once a response IQ has + * been received. + */ + return new Promise((resolve, reject) => { + this.fetchRoomConfiguration().then((stanza) => { + const configArray = [], + fields = stanza.querySelectorAll('field'), + config = this.get('roomconfig'); + let count = fields.length; + + _.each(fields, (field) => { + const fieldname = field.getAttribute('var').replace('muc#roomconfig_', ''), + type = field.getAttribute('type'); + let value; + if (fieldname in config) { + switch (type) { + case 'boolean': + value = config[fieldname] ? 1 : 0; + break; + case 'list-multi': + // TODO: we don't yet handle "list-multi" types + value = field.innerHTML; + break; + default: + value = config[fieldname]; + } + field.innerHTML = $build('value').t(value); + } + configArray.push(field); + if (!--count) { + this.sendConfiguration(configArray, resolve, reject); + } + }); + }); + }); + }, + + fetchRoomConfiguration () { + /* Send an IQ stanza to fetch the room configuration data. + * Returns a promise which resolves once the response IQ + * has been received. + */ + return new Promise((resolve, reject) => { + _converse.connection.sendIQ( + $iq({ + 'to': this.get('jid'), + 'type': "get" + }).c("query", {xmlns: Strophe.NS.MUC_OWNER}), + resolve, + reject + ); + }); + }, + + sendConfiguration (config, callback, errback) { + /* Send an IQ stanza with the room configuration. + * + * Parameters: + * (Array) config: The room configuration + * (Function) callback: Callback upon succesful IQ response + * The first parameter passed in is IQ containing the + * room configuration. + * The second is the response IQ from the server. + * (Function) errback: Callback upon error IQ response + * The first parameter passed in is IQ containing the + * room configuration. + * The second is the response IQ from the server. + */ + const iq = $iq({to: this.get('jid'), type: "set"}) + .c("query", {xmlns: Strophe.NS.MUC_OWNER}) + .c("x", {xmlns: Strophe.NS.XFORM, type: "submit"}); + _.each(config || [], function (node) { iq.cnode(node).up(); }); + callback = _.isUndefined(callback) ? _.noop : _.partial(callback, iq.nodeTree); + errback = _.isUndefined(errback) ? _.noop : _.partial(errback, iq.nodeTree); + return _converse.connection.sendIQ(iq, callback, errback); + }, + saveAffiliationAndRole (pres) { /* Parse the presence stanza for the current user's * affiliation. @@ -555,6 +692,256 @@ return this; }, + findOccupant (data) { + /* Try to find an existing occupant based on the passed in + * data object. + * + * If we have a JID, we use that as lookup variable, + * otherwise we use the nick. We don't always have both, + * but should have at least one or the other. + */ + const jid = Strophe.getBareJidFromJid(data.jid); + if (jid !== null) { + return this.occupants.where({'jid': jid}).pop(); + } else { + return this.occupants.where({'nick': data.nick}).pop(); + } + }, + + updateOccupantsOnPresence (pres) { + /* Given a presence stanza, update the occupant model + * based on its contents. + * + * Parameters: + * (XMLElement) pres: The presence stanza + */ + const data = this.parsePresence(pres); + if (data.type === 'error') { + return true; + } + const occupant = this.findOccupant(data); + if (data.type === 'unavailable') { + if (occupant) { + // Even before destroying, we set the new data, so + // that we can for example show the + // disconnection message. + occupant.set(data); + } + if (!_.includes(data.states, converse.MUC_NICK_CHANGED_CODE)) { + // We only destroy the occupant if this is not a + // nickname change operation. + if (occupant) { + occupant.destroy(); + } + return; + } + } + const jid = Strophe.getBareJidFromJid(data.jid); + const attributes = _.extend(data, { + 'jid': jid ? jid : undefined, + 'resource': data.jid ? Strophe.getResourceFromJid(data.jid) : undefined + }); + if (occupant) { + occupant.save(attributes); + } else { + this.occupants.create(attributes); + } + }, + + parsePresence (pres) { + const id = Strophe.getResourceFromJid(pres.getAttribute("from")); + const data = { + nick: id, + type: pres.getAttribute("type"), + states: [] + }; + _.each(pres.childNodes, function (child) { + switch (child.nodeName) { + case "status": + data.status = child.textContent || null; + break; + case "show": + data.show = child.textContent || 'online'; + break; + case "x": + if (child.getAttribute("xmlns") === Strophe.NS.MUC_USER) { + _.each(child.childNodes, function (item) { + switch (item.nodeName) { + case "item": + data.affiliation = item.getAttribute("affiliation"); + data.role = item.getAttribute("role"); + data.jid = item.getAttribute("jid"); + data.nick = item.getAttribute("nick") || data.nick; + break; + case "status": + if (item.getAttribute("code")) { + data.states.push(item.getAttribute("code")); + } + } + }); + } + } + }); + return data; + }, + + isDuplicateBasedOnTime (message) { + /* Checks whether a received messages is actually a + * duplicate based on whether it has a "ts" attribute + * with a unix timestamp. + * + * This is used for better integration with Slack's XMPP + * gateway, which doesn't use message IDs but instead the + * aforementioned "ts" attributes. + */ + const entity = _converse.disco_entities.get(_converse.domain); + if (entity.identities.where({'name': "Slack-XMPP"})) { + const ts = message.getAttribute('ts'); + if (_.isNull(ts)) { + return false; + } else { + return this.messages.where({ + 'sender': 'me', + 'message': this.getMessageBody(message) + }).filter( + (msg) => Math.abs(moment(msg.get('time')).diff(moment.unix(ts))) < 5000 + ).length > 0; + } + } + return false; + }, + + isDuplicate (message, original_stanza) { + const msgid = message.getAttribute('id'), + jid = message.getAttribute('from'), + resource = Strophe.getResourceFromJid(jid), + sender = resource && Strophe.unescapeNode(resource) || ''; + if (msgid) { + return this.messages.filter( + // Some bots (like HAL in the prosody chatroom) + // respond to commands with the same ID as the + // original message. So we also check the sender. + (msg) => msg.get('msgid') === msgid && msg.get('fullname') === sender + ).length > 0; + } + return this.isDuplicateBasedOnTime(message); + }, + + fetchFeaturesIfConfigurationChanged (stanza) { + const configuration_changed = stanza.querySelector("status[code='104']"), + logging_enabled = stanza.querySelector("status[code='170']"), + logging_disabled = stanza.querySelector("status[code='171']"), + room_no_longer_anon = stanza.querySelector("status[code='172']"), + room_now_semi_anon = stanza.querySelector("status[code='173']"), + room_now_fully_anon = stanza.querySelector("status[code='173']"); + + if (configuration_changed || logging_enabled || logging_disabled || + room_no_longer_anon || room_now_semi_anon || room_now_fully_anon) { + this.getRoomFeatures(); + } + }, + + onMessage (stanza) { + /* Handler for all MUC messages sent to this chat room. + * + * Parameters: + * (XMLElement) stanza: The message stanza. + */ + this.fetchFeaturesIfConfigurationChanged(stanza); + + const original_stanza = stanza, + forwarded = stanza.querySelector('forwarded'); + let delay; + if (!_.isNull(forwarded)) { + stanza = forwarded.querySelector('message'); + delay = forwarded.querySelector('delay'); + } + const jid = stanza.getAttribute('from'), + resource = Strophe.getResourceFromJid(jid), + sender = resource && Strophe.unescapeNode(resource) || '', + subject = _.propertyOf(stanza.querySelector('subject'))('textContent'); + + if (this.isDuplicate(stanza, original_stanza)) { + return; + } + if (subject) { + u.safeSave(this, {'subject': {'author': sender, 'text': subject}}); + } + if (sender === '') { + return; + } + this.incrementUnreadMsgCounter(original_stanza); + this.createMessage(stanza, delay, original_stanza); + if (sender !== this.get('nick')) { + // We only emit an event if it's not our own message + _converse.emit('message', {'stanza': original_stanza, 'chatbox': this}); + } + }, + + onPresence (pres) { + /* Handles all MUC presence stanzas. + * + * Parameters: + * (XMLElement) pres: The stanza + */ + if (pres.getAttribute('type') === 'error') { + this.save('connection_status', converse.ROOMSTATUS.DISCONNECTED); + return; + } + const is_self = pres.querySelector("status[code='110']"); + if (is_self && pres.getAttribute('type') !== 'unavailable') { + this.onOwnPresence(pres); + } + this.updateOccupantsOnPresence(pres); + if (this.get('role') !== 'none' && this.get('connection_status') === converse.ROOMSTATUS.CONNECTING) { + this.save('connection_status', converse.ROOMSTATUS.CONNECTED); + } + }, + + onOwnPresence (pres) { + /* Handles a received presence relating to the current + * user. + * + * For locked rooms (which are by definition "new"), the + * room will either be auto-configured or created instantly + * (with default config) or a configuration room will be + * rendered. + * + * If the room is not locked, then the room will be + * auto-configured only if applicable and if the current + * user is the room's owner. + * + * Parameters: + * (XMLElement) pres: The stanza + */ + this.saveAffiliationAndRole(pres); + + const locked_room = pres.querySelector("status[code='201']"); + if (locked_room) { + if (this.get('auto_configure')) { + this.autoConfigureChatRoom().then(this.getRoomFeatures.bind(this)); + } else if (_converse.muc_instant_rooms) { + // Accept default configuration + this.saveConfiguration().then(this.getRoomFeatures.bind(this)); + } else { + this.trigger('configurationNeeded'); + return; // We haven't yet entered the room, so bail here. + } + } else if (!this.get('features_fetched')) { + // The features for this room weren't fetched. + // That must mean it's a new room without locking + // (in which case Prosody doesn't send a 201 status), + // otherwise the features would have been fetched in + // the "initialize" method already. + if (this.get('affiliation') === 'owner' && this.get('auto_configure')) { + this.autoConfigureChatRoom().then(this.getRoomFeatures.bind(this)); + } else { + this.getRoomFeatures(); + } + } + this.save('connection_status', converse.ROOMSTATUS.ENTERED); + }, + isUserMentioned (message) { /* Returns a boolean to indicate whether the current user * was mentioned in a message. @@ -725,7 +1112,7 @@ _converse.chatboxviews.each(function (view) { if (view.model.get('type') === converse.CHATROOMS_TYPE) { view.model.save('connection_status', converse.ROOMSTATUS.DISCONNECTED); - view.registerHandlers(); + view.model.registerHandlers(); view.join(); view.fetchMessages(); } diff --git a/src/converse-roomslist.js b/src/converse-roomslist.js index bc2b8b551..6aa1bc6e2 100644 --- a/src/converse-roomslist.js +++ b/src/converse-roomslist.js @@ -213,7 +213,8 @@ const name = ev.target.getAttribute('data-room-name'); const jid = ev.target.getAttribute('data-room-jid'); if (confirm(__("Are you sure you want to leave the room %1$s?", name))) { - _converse.chatboxviews.get(jid).leave(); + // TODO: replace with API call + _converse.chatboxviews.get(jid).close(); } },