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
This commit is contained in:
parent
b0c22d983c
commit
9528d81c00
16
CHANGES.md
16
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.
|
||||
|
@ -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); }
|
||||
|
@ -7647,6 +7647,9 @@ 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,
|
||||
#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); }
|
||||
@ -7788,8 +7791,6 @@ 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 {
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
|
||||
|
@ -78,7 +78,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
.toggle-smiley {
|
||||
padding-left: 0.5em;
|
||||
ul {
|
||||
&.emoji-toolbar {
|
||||
.emoji-category-picker {
|
||||
|
252
spec/chatroom.js
252
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 @@
|
||||
* </x>
|
||||
* </presence></body>
|
||||
*/
|
||||
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,9 +615,25 @@
|
||||
null, ['rosterGroupsFetched'], {},
|
||||
function (done, _converse) {
|
||||
|
||||
test_utils.openChatRoom(_converse, "coven", 'chat.shakespeare.lit', 'some1');
|
||||
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).find('.chat-content');
|
||||
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 baseTime = new Date();
|
||||
jasmine.clock().install();
|
||||
jasmine.clock().mockDate(baseTime);
|
||||
var ONE_DAY_LATER = 86400000;
|
||||
jasmine.clock().tick(ONE_DAY_LATER);
|
||||
|
||||
/* <presence to="dummy@localhost/_converse.js-29092160"
|
||||
* from="coven@chat.shakespeare.lit/some1">
|
||||
@ -633,21 +649,21 @@
|
||||
}).c('x', {xmlns: Strophe.NS.MUC_USER})
|
||||
.c('item', {
|
||||
'affiliation': 'owner',
|
||||
'jid': 'dummy@localhost/_converse.js-29092160',
|
||||
'jid': 'some1@localhost/_converse.js-290929789',
|
||||
'role': 'moderator'
|
||||
}).up()
|
||||
.c('status', {code: '110'});
|
||||
});
|
||||
_converse.connection._dataRecv(test_utils.createRequest(presence));
|
||||
|
||||
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");
|
||||
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"
|
||||
);
|
||||
|
||||
// XXX: Hack. We clear the chat contents instead of mocking the date
|
||||
$chat_content.html('');
|
||||
jasmine.clock().tick(ONE_DAY_LATER);
|
||||
|
||||
// Test a user leaving a chat room
|
||||
presence = $pres({
|
||||
@ -664,18 +680,17 @@
|
||||
});
|
||||
_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(
|
||||
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"');
|
||||
|
||||
// XXX: Hack. We clear the chat contents instead of mocking the date
|
||||
$chat_content.html('');
|
||||
jasmine.clock().tick(ONE_DAY_LATER);
|
||||
|
||||
var stanza = Strophe.xmlHtmlNode(
|
||||
'<message xmlns="jabber:client"' +
|
||||
@ -683,7 +698,7 @@
|
||||
' type="groupchat"' +
|
||||
' from="coven@chat.shakespeare.lit/some1">'+
|
||||
' <body>hello world</body>'+
|
||||
' <delay xmlns="urn:xmpp:delay" stamp="2018-01-01T09:35:39Z" from="some1@localhost"/>'+
|
||||
' <delay xmlns="urn:xmpp:delay" stamp="'+moment().format()+'" from="some1@localhost"/>'+
|
||||
'</message>').firstChild;
|
||||
_converse.connection._dataRecv(test_utils.createRequest(stanza));
|
||||
|
||||
@ -698,18 +713,17 @@
|
||||
});
|
||||
_converse.connection._dataRecv(test_utils.createRequest(presence));
|
||||
|
||||
$time = $chat_content.find('time');
|
||||
expect($time.length).toEqual(2);
|
||||
var $time = $chat_content.find('time');
|
||||
expect($time.length).toEqual(4);
|
||||
|
||||
$time = $chat_content.find('time:eq(1)');
|
||||
$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(1);
|
||||
expect($chat_content.find('div.chat-info:first').html()).toBe("newguy has entered the room");
|
||||
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");
|
||||
|
||||
// XXX: Hack. We clear the chat contents instead of mocking the date
|
||||
$chat_content.html('');
|
||||
jasmine.clock().tick(ONE_DAY_LATER);
|
||||
|
||||
stanza = Strophe.xmlHtmlNode(
|
||||
'<message xmlns="jabber:client"' +
|
||||
@ -717,38 +731,43 @@
|
||||
' type="groupchat"' +
|
||||
' from="coven@chat.shakespeare.lit/some1">'+
|
||||
' <body>hello world</body>'+
|
||||
' <delay xmlns="urn:xmpp:delay" stamp="2018-01-01T09:35:39Z" from="some1@localhost"/>'+
|
||||
' <delay xmlns="urn:xmpp:delay" stamp="'+moment().format()+'" from="some1@localhost"/>'+
|
||||
'</message>').firstChild;
|
||||
_converse.connection._dataRecv(test_utils.createRequest(stanza));
|
||||
|
||||
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'
|
||||
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': 'some1@localhost/_converse.js-290929789',
|
||||
'jid': 'newguy@localhost/_converse.js-290929789',
|
||||
'role': 'none'
|
||||
});
|
||||
_converse.connection._dataRecv(test_utils.createRequest(presence));
|
||||
|
||||
$time = $chat_content.find('time');
|
||||
expect($time.length).toEqual(2);
|
||||
expect($time.length).toEqual(6);
|
||||
|
||||
$time = $chat_content.find('time:eq(1)');
|
||||
$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(1);
|
||||
expect($chat_content.find('div.chat-info').length).toBe(6);
|
||||
expect($chat_content.find('div.chat-info:last').html()).toBe(
|
||||
'some1 has left the room. '+
|
||||
'newguy has left the room. '+
|
||||
'"Disconnected: Replaced by new connection"');
|
||||
|
||||
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 = '<img src="x" onerror="alert(\'XSS\');"/>';
|
||||
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 view = _converse.chatboxviews.get('problematic@muc.localhost');
|
||||
spyOn(view, 'renderPasswordForm').and.callThrough();
|
||||
|
||||
var presence = $pres().attrs({
|
||||
from:'lounge@localhost/thirdwitch',
|
||||
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'}).nodeTree;
|
||||
.c('not-authorized').attrs({xmlns:'urn:ietf:params:xml:ns:xmpp-stanzas'});
|
||||
|
||||
var view = _converse.chatboxviews.get('problematic@muc.localhost');
|
||||
spyOn(view, 'renderPasswordForm').and.callThrough();
|
||||
view.onChatRoomPresence(presence);
|
||||
_converse.connection._dataRecv(test_utils.createRequest(presence));
|
||||
|
||||
var $chat_body = $(view.el).find('.chatroom-body');
|
||||
expect(view.renderPasswordForm).toHaveBeenCalled();
|
||||
@ -2636,7 +2632,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'})
|
||||
@ -2645,7 +2641,7 @@
|
||||
.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,7 +2655,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'})
|
||||
@ -2668,7 +2664,7 @@
|
||||
.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,7 +2678,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'})
|
||||
@ -2691,7 +2687,7 @@
|
||||
.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,7 +2767,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'})
|
||||
@ -2778,7 +2776,7 @@
|
||||
.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,7 +2790,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'})
|
||||
@ -2801,7 +2799,7 @@
|
||||
.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,7 +2813,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'})
|
||||
@ -2824,7 +2822,7 @@
|
||||
.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');
|
||||
|
@ -39,7 +39,7 @@
|
||||
</forwarded>
|
||||
</result>
|
||||
</message>`).firstElementChild;
|
||||
chatroomview.onChatRoomMessage(stanza);
|
||||
chatroomview.model.onMessage(stanza);
|
||||
expect(chatroomview.content.querySelectorAll('.chat-message').length).toBe(1);
|
||||
done();
|
||||
});
|
||||
|
@ -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');
|
||||
|
@ -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();
|
||||
|
@ -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(),
|
||||
|
@ -416,6 +416,7 @@
|
||||
'isodate': isodate,
|
||||
'data': data
|
||||
}));
|
||||
this.insertDayIndicator(this.content.lastElementChild);
|
||||
this.scrollDown();
|
||||
return isodate;
|
||||
},
|
||||
|
@ -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(
|
||||
|
@ -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,64 +994,12 @@
|
||||
* (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.
|
||||
this.model.join(nick, password);
|
||||
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);
|
||||
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) {
|
||||
@ -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,9 +1353,12 @@
|
||||
}
|
||||
},
|
||||
|
||||
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') &&
|
||||
@ -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 <message> 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(
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user