Commit 879e165a authored by JC Brand's avatar JC Brand

Refactoring

- Move headless one-on-one chat functionality into converse-chat
- Split converse-headline into converse-headlines and converse-headlines-views
- Add api in `_converse.api.chatboxes` for creating chatboxes
- Add `_converse.api.controlbox.get` method
parent 93d56898
......@@ -14,6 +14,7 @@
instances. Still working out a wire protocol for compatibility with other clients.
To add custom emojis, edit the `emojis.json` file.
- Refactor some presence and status handling code from `converse-core` into `@converse/headless/converse-status`.
- New API [\_converse.api.headlines](https://conversejs.org/docs/html/api/-_converse.api.headlines.html#.get)
### Breaking changes
......@@ -34,10 +35,12 @@
* `_converse.api.rooms.create`
* `_converse.api.roomviews.close`
- `_converse.api.chats.get()` now only returns one-on-one chats, not the control box or headline notifications.
- The `show_only_online_users` setting has been removed.
- The order of certain events have now changed: `statusInitialized` is now triggered after `initialized` and `connected` and `reconnected`.
- `_converse.api.alert.show` is now `_converse.api.show` and instead of taking
an integer for the `type`, "info", "warn" or "error" should be passed in.
- The `converse-headline` plugin has been split up into `converse-headlines` and `converse-headlines-view`.
## 5.0.4 (2019-10-08)
- New config option [allow_message_corrections](https://conversejs.org/docs/html/configuration.html#allow-message-corrections)
......
......@@ -168,7 +168,7 @@
'name': 'The Play',
'nick': ' Othello'
});
await u.waitUntil(() => _converse.api.rooms.get().length);
await new Promise(resolve => _converse.api.listen.once('chatBoxInitialized', resolve));
expect(_.isUndefined(_converse.chatboxviews.get(jid))).toBeFalsy();
// Check that we don't auto-join if muc_respect_autojoin is false
......
......@@ -38,7 +38,7 @@
type: 'chat',
id: (new Date()).getTime()
}).c('body').t('hello world').tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
expect(view.content.lastElementChild.textContent.trim().indexOf('hello world')).not.toBe(-1);
done();
......@@ -61,7 +61,7 @@
}).c('body').t(message).up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
const view = _converse.chatboxviews.get(sender_jid);
await new Promise(resolve => view.once('messageInserted', resolve));
expect(view.el.querySelectorAll('.chat-msg--action').length).toBe(1);
......@@ -135,7 +135,7 @@
const message_promise = new Promise(resolve => _converse.api.listen.on('message', resolve));
_converse.connection._dataRecv(test_utils.createRequest(stanza));
await u.waitUntil(() => _converse.api.chats.get().length === 2);
await new Promise(resolve => _converse.api.listen.once('chatBoxInitialized', resolve));
await u.waitUntil(() => message_promise);
expect(_converse.chatboxviews.keys().length).toBe(2);
done();
......@@ -190,7 +190,7 @@
el.click();
}
await u.waitUntil(() => _converse.chatboxes.length == 16);
expect(_converse.chatboxviews.trimChats.calls.count()).toBe(16);
expect(_converse.chatboxviews.trimChats.calls.count()).toBe(17);
_converse.api.chatviews.get().forEach(v => spyOn(v, 'onMinimized').and.callThrough());
for (i=0; i<online_contacts.length; i++) {
......@@ -212,7 +212,7 @@
expect(trimmedview.restore).toHaveBeenCalled();
expect(chatbox.maximize).toHaveBeenCalled();
expect(_converse.chatboxviews.trimChats.calls.count()).toBe(17);
expect(_converse.chatboxviews.trimChats.calls.count()).toBe(18);
done();
}));
......@@ -520,58 +520,28 @@
describe("A Chat Status Notification", function () {
it("is ignored when it's a carbon copy of one of my own",
mock.initConverse(
['rosterGroupsFetched'], {},
async function (done, _converse) {
await test_utils.waitForRoster(_converse, 'current');
await test_utils.openControlBox(_converse);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await test_utils.openChatBoxFor(_converse, sender_jid);
let stanza = u.toStanza(
`<message from="${sender_jid}"
type="chat"
to="romeo@montague.lit/orchard">
<composing xmlns="http://jabber.org/protocol/chatstates"/>
<no-store xmlns="urn:xmpp:hints"/>
<no-permanent-store xmlns="urn:xmpp:hints"/>
</message>`);
_converse.connection._dataRecv(test_utils.createRequest(stanza));
stanza = u.toStanza(
`<message from="${sender_jid}"
type="chat"
to="romeo@montague.lit/orchard">
<paused xmlns="http://jabber.org/protocol/chatstates"/>
<no-store xmlns="urn:xmpp:hints"/>
<no-permanent-store xmlns="urn:xmpp:hints"/>
</message>`);
_converse.connection._dataRecv(test_utils.createRequest(stanza));
done();
}));
it("does not open a new chatbox",
mock.initConverse(
['rosterGroupsFetched'], {},
['rosterGroupsFetched', 'emojisInitialized'], {},
async function (done, _converse) {
await test_utils.waitForRoster(_converse, 'current');
await test_utils.openControlBox(_converse);
spyOn(_converse.api, "trigger").and.callThrough();
const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
// <composing> state
const msg = $msg({
const stanza = $msg({
'from': sender_jid,
'to': _converse.connection.jid,
'type': 'chat',
'id': (new Date()).getTime()
}).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
await _converse.chatboxes.onMessage(msg);
spyOn(_converse.api, "trigger").and.callThrough();
_converse.connection._dataRecv(test_utils.createRequest(stanza));
await u.waitUntil(() => _converse.api.trigger.calls.count());
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
expect(_converse.api.chats.get().length).toBe(1);
expect(_converse.chatboxviews.keys().length).toBe(1);
done();
}));
......@@ -705,7 +675,6 @@
await test_utils.openControlBox(_converse);
// See XEP-0085 https://xmpp.org/extensions/xep-0085.html#definitions
spyOn(_converse.api, "trigger").and.callThrough();
const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length);
await test_utils.openChatBoxFor(_converse, sender_jid);
......@@ -716,10 +685,13 @@
to: _converse.connection.jid,
type: 'chat',
id: (new Date()).getTime()
}).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
await _converse.chatboxes.onMessage(msg);
}).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
spyOn(_converse.api, "trigger").and.callThrough();
_converse.connection._dataRecv(test_utils.createRequest(msg));
await u.waitUntil(() => _converse.api.trigger.calls.count());
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
var view = _converse.chatboxviews.get(sender_jid);
const view = _converse.chatboxviews.get(sender_jid);
expect(view).toBeDefined();
const event = await u.waitUntil(() => view.el.querySelector('.chat-state-notification'));
......@@ -732,7 +704,7 @@
type: 'chat',
id: (new Date()).getTime()
}).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
const events = view.el.querySelectorAll('.chat-state-notification');
expect(events.length).toBe(1);
expect(events[0].textContent).toEqual(mock.cur_names[1] + ' is typing');
......@@ -765,7 +737,7 @@
'to': recipient_jid,
'type': 'chat'
}).c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.model.messages.length);
// Check that the chatbox and its view now exist
const chatbox = _converse.chatboxes.get(recipient_jid);
......@@ -859,7 +831,7 @@
type: 'chat',
id: (new Date()).getTime()
}).c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
await u.waitUntil(() => view.model.vcard.get('fullname') === mock.cur_names[1])
const event = await u.waitUntil(() => view.el.querySelector('.chat-state-notification'));
......@@ -893,7 +865,7 @@
'to': recipient_jid,
'type': 'chat'
}).c('paused', {'xmlns': Strophe.NS.CHATSTATES}).tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.model.messages.length);
// Check that the chatbox and its view now exist
const chatbox = _converse.chatboxes.get(recipient_jid);
......@@ -1042,7 +1014,8 @@
'type': 'chat'})
.c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
.tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.model.messages.length);
await u.waitUntil(() => view.el.querySelector('.chat-state-notification'));
expect(view.el.querySelectorAll('.chat-state-notification').length).toBe(1);
msg = $msg({
......@@ -1050,8 +1023,8 @@
to: _converse.connection.jid,
type: 'chat',
id: (new Date()).getTime()
}).c('body').c('inactive', {'xmlns': Strophe.NS.CHATSTATES}).tree();
await _converse.chatboxes.onMessage(msg);
}).c('inactive', {'xmlns': Strophe.NS.CHATSTATES}).tree();
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => (view.model.messages.length > 1));
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
await u.waitUntil(() => view.el.querySelectorAll('.chat-state-notification').length === 0);
......@@ -1078,7 +1051,7 @@
type: 'chat',
id: (new Date()).getTime()
}).c('body').c('gone', {'xmlns': Strophe.NS.CHATSTATES}).tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
const view = _converse.chatboxviews.get(sender_jid);
await u.waitUntil(() => view.model.vcard.get('fullname') === mock.cur_names[1]);
......@@ -1165,7 +1138,7 @@
spyOn(_converse, 'incrementMsgCounter').and.callThrough();
spyOn(_converse, 'clearMsgCounter').and.callThrough();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.once('messageInserted', resolve));
expect(_converse.incrementMsgCounter).toHaveBeenCalled();
expect(_converse.clearMsgCounter).not.toHaveBeenCalled();
......@@ -1210,7 +1183,7 @@
id: (new Date()).getTime()
}).c('body').t(message).up()
.c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
expect(_converse.incrementMsgCounter).not.toHaveBeenCalled();
expect(document.title).toBe('Converse Tests');
done();
......@@ -1240,14 +1213,13 @@
// leave converse-chat page
_converse.windowState = 'hidden';
_converse.chatboxes.onMessage(msgFactory());
await u.waitUntil(() => _converse.api.chats.get().length === 2)
await _converse.handleMessageStanza(msgFactory());
let view = _converse.chatboxviews.get(sender_jid);
expect(document.title).toBe('Messages (1) Converse Tests');
// come back to converse-chat page
_converse.saveWindowState(null, 'focus');
expect(u.isVisible(view.el)).toBeTruthy();
await u.waitUntil(() => u.isVisible(view.el));
expect(document.title).toBe('Converse Tests');
// close chatbox and leave converse-chat page again
......@@ -1255,8 +1227,7 @@
_converse.windowState = 'hidden';
// check that msg_counter is incremented from zero again
_converse.chatboxes.onMessage(msgFactory());
await u.waitUntil(() => _converse.api.chats.get().length === 2)
await _converse.handleMessageStanza(msgFactory());
view = _converse.chatboxviews.get(sender_jid);
expect(u.isVisible(view.el)).toBeTruthy();
expect(document.title).toBe('Messages (1) Converse Tests');
......@@ -1277,7 +1248,7 @@
const view = await test_utils.openChatBoxFor(_converse, sender_jid)
view.model.save('scrolled', true);
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.model.messages.length);
expect(view.model.get('num_unread')).toBe(1);
done();
......@@ -1295,7 +1266,7 @@
await test_utils.openChatBoxFor(_converse, sender_jid);
const chatbox = _converse.chatboxes.get(sender_jid);
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
expect(chatbox.get('num_unread')).toBe(0);
done();
}));
......@@ -1313,7 +1284,7 @@
await test_utils.openChatBoxFor(_converse, sender_jid);
const chatbox = _converse.chatboxes.get(sender_jid);
_converse.windowState = 'hidden';
_converse.chatboxes.onMessage(msgFactory());
_converse.handleMessageStanza(msgFactory());
await u.waitUntil(() => chatbox.messages.length);
expect(chatbox.get('num_unread')).toBe(1);
done();
......@@ -1331,7 +1302,7 @@
const chatbox = _converse.chatboxes.get(sender_jid);
chatbox.save('scrolled', true);
_converse.windowState = 'hidden';
_converse.chatboxes.onMessage(msgFactory());
_converse.handleMessageStanza(msgFactory());
await u.waitUntil(() => chatbox.messages.length);
expect(chatbox.get('num_unread')).toBe(1);
done();
......@@ -1348,7 +1319,7 @@
await test_utils.openChatBoxFor(_converse, sender_jid);
const chatbox = _converse.chatboxes.get(sender_jid);
_converse.windowState = 'hidden';
_converse.chatboxes.onMessage(msgFactory());
_converse.handleMessageStanza(msgFactory());
await u.waitUntil(() => chatbox.messages.length);
expect(chatbox.get('num_unread')).toBe(1);
_converse.saveWindowState(null, 'focus');
......@@ -1368,7 +1339,7 @@
const chatbox = _converse.chatboxes.get(sender_jid);
chatbox.save('scrolled', true);
_converse.windowState = 'hidden';
_converse.chatboxes.onMessage(msgFactory());
_converse.handleMessageStanza(msgFactory());
await u.waitUntil(() => chatbox.messages.length);
expect(chatbox.get('num_unread')).toBe(1);
_converse.saveWindowState(null, 'focus');
......@@ -1392,13 +1363,13 @@
const chatbox = _converse.chatboxes.get(sender_jid);
chatbox.save('scrolled', true);
msg = test_utils.createChatMessage(_converse, sender_jid, 'This message will be unread');
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => chatbox.messages.length);
const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
indicator_el = sizzle(selector, _converse.rosterview.el).pop();
expect(indicator_el.textContent).toBe('1');
msg = test_utils.createChatMessage(_converse, sender_jid, 'This message will be unread too');
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => chatbox.messages.length > 1);
indicator_el = sizzle(selector, _converse.rosterview.el).pop();
expect(indicator_el.textContent).toBe('2');
......@@ -1421,14 +1392,14 @@
chatboxview.minimize();
msg = test_utils.createChatMessage(_converse, sender_jid, 'This message will be unread');
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => chatbox.messages.length);
const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
indicator_el = sizzle(selector, _converse.rosterview.el).pop();
expect(indicator_el.textContent).toBe('1');
msg = test_utils.createChatMessage(_converse, sender_jid, 'This message will be unread too');
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => chatbox.messages.length === 2);
indicator_el = sizzle(selector, _converse.rosterview.el).pop();
expect(indicator_el.textContent).toBe('2');
......@@ -1450,10 +1421,10 @@
const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
const select_msgs_indicator = () => sizzle(selector, _converse.rosterview.el).pop();
view.minimize();
_converse.chatboxes.onMessage(msgFactory());
_converse.handleMessageStanza(msgFactory());
await u.waitUntil(() => chatbox.messages.length);
expect(select_msgs_indicator().textContent).toBe('1');
_converse.chatboxes.onMessage(msgFactory());
_converse.handleMessageStanza(msgFactory());
await u.waitUntil(() => chatbox.messages.length > 1);
expect(select_msgs_indicator().textContent).toBe('2');
view.model.maximize();
......@@ -1476,7 +1447,7 @@
const selector = `a.open-chat:contains("${chatbox.get('nickname')}") .msgs-indicator`;
const select_msgs_indicator = () => sizzle(selector, _converse.rosterview.el).pop();
chatbox.save('scrolled', true);
_converse.chatboxes.onMessage(msgFactory());
_converse.handleMessageStanza(msgFactory());
const view = _converse.chatboxviews.get(sender_jid);
await u.waitUntil(() => view.model.messages.length);
expect(select_msgs_indicator().textContent).toBe('1');
......@@ -1502,7 +1473,7 @@
const selector = 'a.open-chat:contains("' + chatbox.get('nickname') + '") .msgs-indicator';
const select_msgs_indicator = () => sizzle(selector, _converse.rosterview.el).pop();
chatbox.save('scrolled', true);
_converse.chatboxes.onMessage(msgFactory());
_converse.handleMessageStanza(msgFactory());
await u.waitUntil(() => view.model.messages.length);
expect(select_msgs_indicator().textContent).toBe('1');
await test_utils.openChatBoxFor(_converse, sender_jid);
......@@ -1530,7 +1501,7 @@
};
const chatbox = _converse.chatboxes.get(sender_jid);
chatbox.save('scrolled', true);
_converse.chatboxes.onMessage(msgFactory());
_converse.handleMessageStanza(msgFactory());
await u.waitUntil(() => chatbox.messages.length);
const chatboxview = _converse.chatboxviews.get(sender_jid);
chatboxview.minimize();
......@@ -1558,7 +1529,7 @@
return minimizedChatBoxView.el.querySelector('.message-count');
};
view.minimize();
_converse.chatboxes.onMessage(msgFactory());
_converse.handleMessageStanza(msgFactory());
await u.waitUntil(() => view.model.messages.length);
const unread_count = selectUnreadMsgCount();
expect(u.isVisible(unread_count)).toBeTruthy();
......
......@@ -88,7 +88,7 @@
id: (new Date()).getTime()
}).c('body').t('hello').up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
_converse.chatboxes.onMessage(msg);
_converse.handleMessageStanza(msg);
await u.waitUntil(() => _converse.rosterview.el.querySelectorAll(".msgs-indicator").length);
spyOn(chatview.model, 'incrementUnreadMsgCounter').and.callThrough();
expect(_converse.chatboxviews.el.querySelector('.restore-chat .message-count').textContent).toBe('1');
......@@ -101,7 +101,7 @@
id: (new Date()).getTime()
}).c('body').t('hello again').up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
_converse.chatboxes.onMessage(msg);
_converse.handleMessageStanza(msg);
await u.waitUntil(() => chatview.model.incrementUnreadMsgCounter.calls.count());
expect(_converse.chatboxviews.el.querySelector('.restore-chat .message-count').textContent).toBe('2');
expect(_converse.rosterview.el.querySelector('.msgs-indicator').textContent).toBe('2');
......
......@@ -269,7 +269,7 @@
// Test on chat that's not open
chat = await _converse.api.chats.get(jid);
expect(typeof chat === 'undefined').toBeTruthy();
expect(chat === null).toBeTruthy();
expect(_converse.chatboxes.length).toBe(1);
// Test for one JID
......@@ -281,7 +281,7 @@
await u.waitUntil(() => u.isVisible(view.el));
// Test for multiple JIDs
test_utils.openChatBoxFor(_converse, jid2);
await u.waitUntil(() => _converse.chatboxes.length == 2);
await u.waitUntil(() => _converse.chatboxes.length == 3);
const list = await _converse.api.chats.get([jid, jid2]);
expect(Array.isArray(list)).toBeTruthy();
expect(list[0].get('box_id')).toBe(`box-${btoa(jid)}`);
......
......@@ -163,7 +163,7 @@
await test_utils.waitForRoster(_converse, 'current');
const sender_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
_converse.chatboxes.onMessage($msg({
_converse.handleMessageStanza($msg({
'from': sender_jid,
'to': _converse.connection.jid,
'type': 'chat',
......@@ -177,7 +177,7 @@
let message = chat_content.querySelector('.chat-msg__text');
expect(u.hasClass('chat-msg__text--larger', message)).toBe(true);
_converse.chatboxes.onMessage($msg({
_converse.handleMessageStanza($msg({
'from': sender_jid,
'to': _converse.connection.jid,
'type': 'chat',
......
......@@ -14,7 +14,7 @@
it("will not open nor display non-headline messages",
mock.initConverse(
['rosterGroupsFetched', 'chatBoxesFetched'], {}, async function (done, _converse) {
['rosterGroupsFetched', 'chatBoxesFetched'], {}, function (done, _converse) {
/* XMPP spam message:
*
......@@ -36,9 +36,9 @@
.c('nick', {'xmlns': "http://jabber.org/protocol/nick"}).t("-wwdmz").up()
.c('body').t('SORRY FOR THIS ADVERT');
_converse.connection._dataRecv(test_utils.createRequest(stanza));
await u.waitUntil(() => _converse.api.chats.get().length);
expect(u.isHeadlineMessage.called).toBeTruthy();
expect(u.isHeadlineMessage.returned(false)).toBeTruthy();
expect(_converse.api.headlines.get().length === 0);
u.isHeadlineMessage.restore();
done();
}));
......
......@@ -205,7 +205,7 @@
describe("An archived message", function () {
describe("when recieved", function () {
describe("when received", function () {
it("is discarded if it doesn't come from the right sender",
mock.initConverse(
......
......@@ -21,7 +21,8 @@
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const forwarded_contact_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await test_utils.openChatBoxFor(_converse, contact_jid);
expect(_converse.api.chats.get().length).toBe(2);
let models = await _converse.api.chats.get();
expect(models.length).toBe(1);
const received_stanza = u.toStanza(`
<message to='${_converse.jid}' from='${contact_jid}' type='chat' id='${_converse.connection.getUniqueId()}'>
<body>A most courteous exposition!</body>
......@@ -51,7 +52,8 @@
'Forwarded messages not part of an encapsulating protocol are not supported</text>'+
'</error>'+
'</message>');
expect(_converse.api.chats.get().length).toBe(2);
models = await _converse.api.chats.get();
expect(models.length).toBe(1);
done();
}));
......@@ -148,7 +150,7 @@
await u.waitUntil(() => (u.hasClass('correcting', view.el.querySelector('.chat-msg')) === false), 500);
// Test that messages from other users don't have the pencil icon
_converse.chatboxes.onMessage(
_converse.handleMessageStanza(
$msg({
'from': contact_jid,
'to': _converse.connection.jid,
......@@ -352,7 +354,6 @@
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length)
spyOn(_converse.chatboxes, 'getChatBox').and.callThrough();
_converse.filter_by_resource = true;
let msg = $msg({
......@@ -364,8 +365,7 @@
.c('body').t("message").up()
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T13:08:25Z'})
.tree();
await _converse.chatboxes.onMessage(msg);
await u.waitUntil(() => _converse.api.chats.get().length);
await _converse.handleMessageStanza(msg);
const view = _converse.api.chatviews.get(sender_jid);
msg = $msg({
......@@ -377,7 +377,7 @@
.c('body').t("Older message").up()
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2017-12-31T22:08:25Z'})
.tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.once('messageInserted', resolve));
msg = $msg({
......@@ -389,7 +389,7 @@
.c('body').t("Inbetween message").up()
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'})
.tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.once('messageInserted', resolve));
msg = $msg({
......@@ -401,7 +401,7 @@
.c('body').t("another inbetween message").up()
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-01T13:18:23Z'})
.tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.once('messageInserted', resolve));
msg = $msg({
......@@ -413,7 +413,7 @@
.c('body').t("An earlier message on the next day").up()
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T12:18:23Z'})
.tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.once('messageInserted', resolve));
msg = $msg({
......@@ -425,7 +425,7 @@
.c('body').t("newer message from the next day").up()
.c('delay', {'xmlns': 'urn:xmpp:delay', 'stamp':'2018-01-02T22:28:23Z'})
.tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.once('messageInserted', resolve));
// Insert <composing> message, to also check that
......@@ -439,7 +439,7 @@
'type': 'chat'})
.c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
.tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
msg = $msg({
'id': _converse.connection.getUniqueId(),
......@@ -450,7 +450,7 @@
.c('composing', {'xmlns': Strophe.NS.CHATSTATES}).up()
.c('body').t("latest message")
.tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.once('messageInserted', resolve));
const chat_content = view.el.querySelector('.chat-content');
......@@ -519,7 +519,7 @@
// Ideally we wouldn't have to filter out headline
// messages, but Prosody gives them the wrong 'type' :(
sinon.spy(_converse, 'log');
sinon.spy(_converse.chatboxes, 'getChatBox');
sinon.spy(_converse.api.chatboxes, 'get');
sinon.spy(u, 'isHeadlineMessage');
const msg = $msg({
from: 'montague.lit',
......@@ -527,17 +527,17 @@
type: 'chat',
id: (new Date()).getTime()
}).c('body').t("This headline message will not be shown").tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
expect(_converse.log.calledWith(
"onMessage: Ignoring incoming headline message from JID: montague.lit",
Strophe.LogLevel.INFO
)).toBeTruthy();
expect(u.isHeadlineMessage.called).toBeTruthy();
expect(u.isHeadlineMessage.returned(true)).toBeTruthy();
expect(_converse.chatboxes.getChatBox.called).toBeFalsy();
expect(_converse.api.chatboxes.get.called).toBeFalsy();
// Remove sinon spies
_converse.log.restore();
_converse.chatboxes.getChatBox.restore();
_converse.api.chatboxes.get.restore();
u.isHeadlineMessage.restore();
done();
}));
......@@ -570,8 +570,7 @@
'type': 'chat'
}).c('body').t(msgtext).tree();
await _converse.chatboxes.onMessage(msg);
await u.waitUntil(() => (_converse.api.chats.get().length > 1))
await _converse.handleMessageStanza(msg);
const chatbox = _converse.chatboxes.get(sender_jid);
const view = _converse.chatboxviews.get(sender_jid);
......@@ -622,7 +621,7 @@
'type': 'chat'
}).c('body').t(msgtext).tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
// Check that the chatbox and its view now exist
const chatbox = await _converse.api.chats.get(recipient_jid);
const view = _converse.api.chatviews.get(recipient_jid);
......@@ -677,15 +676,15 @@
'to': _converse.connection.jid,
'type': 'chat'
}).c('body').t(msgtext).tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
// Check that chatbox for impersonated user is not created.
let chatbox = await _converse.api.chats.get(impersonated_jid);
expect(chatbox).not.toBeDefined();
expect(chatbox).toBe(null);
// Check that the chatbox for the malicous user is not created
chatbox = await _converse.api.chats.get(sender_jid);
expect(chatbox).not.toBeDefined();
expect(chatbox).toBe(null);
done();
}));
......@@ -719,7 +718,7 @@
id: (new Date()).getTime()
}).c('body').t(message).up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => chatview.model.messages.length);
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
......@@ -730,7 +729,7 @@
expect(trimmedview.model.get('minimized')).toBeTruthy();
expect(u.isVisible(count)).toBeTruthy();
expect(count.textContent).toBe('1');
_converse.chatboxes.onMessage(
_converse.handleMessageStanza(
$msg({
from: mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit',
to: _converse.connection.jid,
......@@ -779,7 +778,7 @@
}).c('body').t(message).up()
.c('delay', { xmlns:'urn:xmpp:delay', from: 'montague.lit', stamp: one_day_ago.toISOString() })
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.once('messageInserted', resolve));
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
......@@ -812,7 +811,7 @@
id: new Date().getTime()
}).c('body').t(message).up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
await new Promise(resolve => view.once('messageInserted', resolve));
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
......@@ -1077,7 +1076,7 @@
jasmine.clock().install();
jasmine.clock().mockDate(base_time);
_converse.chatboxes.onMessage($msg({
_converse.handleMessageStanza($msg({
'from': sender_jid,
'to': _converse.connection.jid,
'type': 'chat',
......@@ -1089,7 +1088,7 @@
await new Promise(resolve => view.once('messageInserted', resolve));
jasmine.clock().tick(3*ONE_MINUTE_LATER);
_converse.chatboxes.onMessage($msg({
_converse.handleMessageStanza($msg({
'from': sender_jid,
'to': _converse.connection.jid,
'type': 'chat',
......@@ -1099,7 +1098,7 @@
await new Promise(resolve => view.once('messageInserted', resolve));
jasmine.clock().tick(11*ONE_MINUTE_LATER);
_converse.chatboxes.onMessage($msg({
_converse.handleMessageStanza($msg({
'from': sender_jid,
'to': _converse.connection.jid,
'type': 'chat',
......@@ -1112,7 +1111,7 @@
// Insert <composing> message, to also check that
// text messages are inserted correctly with
// temporary chat events in the chat contents.
_converse.chatboxes.onMessage($msg({
_converse.handleMessageStanza($msg({
'id': 'aeb219',
'to': _converse.bare_jid,
'xmlns': 'jabber:client',
......@@ -1123,7 +1122,7 @@
await new Promise(resolve => view.once('messageInserted', resolve));
jasmine.clock().tick(1*ONE_MINUTE_LATER);
_converse.chatboxes.onMessage($msg({
_converse.handleMessageStanza($msg({
'from': sender_jid,
'to': _converse.connection.jid,
'type': 'chat',
......@@ -1154,7 +1153,7 @@
"Another message within 10 minutes, but from a different person");
// Let's add a delayed, inbetween message
_converse.chatboxes.onMessage(
_converse.handleMessageStanza(
$msg({
'xmlns': 'jabber:client',
'id': _converse.connection.getUniqueId(),
......@@ -1184,7 +1183,7 @@
"Another message 1 minute and 1 second since the previous one");
expect(u.hasClass('chat-msg--followup', chat_content.querySelector('.message:nth-child(7)'))).toBe(false);
_converse.chatboxes.onMessage(
_converse.handleMessageStanza(
$msg({
'xmlns': 'jabber:client',
'id': _converse.connection.getUniqueId(),
......@@ -1242,7 +1241,7 @@
'id': msg_id,
}).c('body').t('Message!').up()
.c('request', {'xmlns': Strophe.NS.RECEIPTS}).tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
const sent_messages = sent_stanzas.map(s => _.isElement(s) ? s : s.nodeTree).filter(s => s.nodeName === 'message');
expect(sent_messages.length).toBe(1);
const receipt = sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, sent_messages[0]).pop();
......@@ -1274,8 +1273,7 @@
'id': msg_id
}).c('body').t('Message!').up()
.c('request', {'xmlns': Strophe.NS.RECEIPTS}).tree();
await _converse.chatboxes.onMessage(msg);
await u.waitUntil(() => _converse.api.chats.get().length);
await _converse.handleMessageStanza(msg);
expect(view.model.sendReceiptStanza).not.toHaveBeenCalled();
done();
}));
......@@ -1298,7 +1296,6 @@
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
});
await u.waitUntil(() => _converse.api.chats.get().length);
const chatbox = _converse.chatboxes.get(contact_jid);
expect(chatbox).toBeDefined();
await new Promise(resolve => view.once('messageInserted', resolve));
......@@ -1314,7 +1311,7 @@
expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(1);
// Also handle receipts with type 'chat'. See #1353
spyOn(_converse.chatboxes, 'onMessage').and.callThrough();
spyOn(_converse, 'handleMessageStanza').and.callThrough();
textarea.value = 'Another message';
view.onKeyDown({
target: textarea,
......@@ -1334,7 +1331,7 @@
_converse.connection._dataRecv(test_utils.createRequest(msg));
await new Promise(resolve => view.model.messages.once('rendered', resolve));
expect(view.el.querySelectorAll('.chat-msg__receipt').length).toBe(2);
expect(_converse.chatboxes.onMessage.calls.count()).toBe(1);
expect(_converse.handleMessageStanza.calls.count()).toBe(1);
done();
}));
......@@ -1396,7 +1393,7 @@
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
// We don't already have an open chatbox for this user
expect(_converse.chatboxes.get(sender_jid)).not.toBeDefined();
_converse.chatboxes.onMessage(
await _converse.handleMessageStanza(
$msg({
'from': sender_jid,
'to': _converse.connection.jid,
......@@ -1405,8 +1402,7 @@
}).c('body').t(message).up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()
);
await u.waitUntil(() => (_converse.api.chats.get().length === 2));
const chatbox = _converse.chatboxes.get(sender_jid);
const chatbox = await _converse.chatboxes.get(sender_jid);
expect(chatbox).toBeDefined();
const view = _converse.api.chatviews.get(sender_jid);
expect(view).toBeDefined();
......@@ -1421,7 +1417,8 @@
expect(msg_obj.get('is_delayed')).toEqual(false);
// Now check that the message appears inside the chatbox in the DOM
const chat_content = view.el.querySelector('.chat-content');
expect(chat_content.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(message);
const mel = await u.waitUntil(() => chat_content.querySelector('.chat-msg .chat-msg__text'));
expect(mel.textContent).toEqual(message);
expect(chat_content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
await u.waitUntil(() => chatbox.vcard.get('fullname') === mock.cur_names[0]);
expect(chat_content.querySelector('span.chat-msg__author').textContent.trim()).toBe('Mercutio');
......@@ -1437,7 +1434,7 @@
await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length, 300);
const message = '\n\n This is a received message \n\n';
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
_converse.chatboxes.onMessage(
await _converse.handleMessageStanza(
$msg({
'from': sender_jid,
'to': _converse.connection.jid,
......@@ -1446,13 +1443,13 @@
}).c('body').t(message).up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()
);
await u.waitUntil(() => (_converse.api.chats.get().length === 2));
const view = _converse.api.chatviews.get(sender_jid);
expect(view.model.messages.length).toEqual(1);
const msg_obj = view.model.messages.at(0);
expect(msg_obj.get('message')).toEqual(message.trim());
const chat_content = view.el.querySelector('.chat-content');
expect(chat_content.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(message.trim());
const mel = await u.waitUntil(() => chat_content.querySelector('.chat-msg .chat-msg__text'));
expect(mel.textContent).toEqual(message.trim());
done();
}));
......@@ -1467,7 +1464,7 @@
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
const msg_id = u.getUniqueId();
const view = await test_utils.openChatBoxFor(_converse, sender_jid);
_converse.chatboxes.onMessage($msg({
_converse.handleMessageStanza($msg({
'from': sender_jid,
'to': _converse.connection.jid,
'type': 'chat',
......@@ -1478,7 +1475,7 @@
expect(view.el.querySelector('.chat-msg__text').textContent)
.toBe('But soft, what light through yonder airlock breaks?');
_converse.chatboxes.onMessage($msg({
_converse.handleMessageStanza($msg({
'from': sender_jid,
'to': _converse.connection.jid,
'type': 'chat',
......@@ -1493,7 +1490,7 @@
expect(view.el.querySelectorAll('.chat-msg__content .fa-edit').length).toBe(1);
expect(view.model.messages.models.length).toBe(1);
_converse.chatboxes.onMessage($msg({
_converse.handleMessageStanza($msg({
'from': sender_jid,
'to': _converse.connection.jid,
'type': 'chat',
......@@ -1550,8 +1547,7 @@
// We don't already have an open chatbox for this user
expect(_converse.chatboxes.get(sender_jid)).not.toBeDefined();
await _converse.chatboxes.onMessage(msg);
await u.waitUntil(() => _converse.api.chats.get().length);
await _converse.handleMessageStanza(msg);
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
// Check that the chatbox and its view now exist
......@@ -1600,15 +1596,15 @@
expect(_converse.chatboxes.get(sender_jid)).not.toBeDefined();
let chatbox = await _converse.api.chats.get(sender_jid);
expect(chatbox).not.toBeDefined();
expect(chatbox).toBe(null);
// onMessage is a handler for received XMPP messages
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
let view = _converse.chatboxviews.get(sender_jid);
expect(view).not.toBeDefined();
// onMessage is a handler for received XMPP messages
_converse.allow_non_roster_messaging = true;
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
view = _converse.chatboxviews.get(sender_jid);
await new Promise(resolve => view.once('messageInserted', resolve));
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
......@@ -1807,7 +1803,7 @@
// Create enough messages so that there's a scrollbar.
const promises = [];
for (let i=0; i<20; i++) {
_converse.chatboxes.onMessage($msg({
_converse.handleMessageStanza($msg({
from: sender_jid,
to: _converse.connection.jid,
type: 'chat',
......@@ -1826,7 +1822,7 @@
view.model.set('scrolled', true);
const message = 'This message is received while the chat area is scrolled up';
_converse.chatboxes.onMessage($msg({
_converse.handleMessageStanza($msg({
from: sender_jid,
to: _converse.connection.jid,
type: 'chat',
......@@ -1858,7 +1854,7 @@
await u.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group').length)
// Send a message from a different resource
spyOn(_converse, 'log');
spyOn(_converse.chatboxes, 'getChatBox').and.callThrough();
spyOn(_converse.api.chatboxes, 'create').and.callThrough();
_converse.filter_by_resource = true;
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
let msg = $msg({
......@@ -1868,12 +1864,12 @@
id: (new Date()).getTime()
}).c('body').t("This message will not be shown").up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
await _converse.chatboxes.onMessage(msg);
await u.waitUntil(() => _converse.api.chats.get().length);
await _converse.handleMessageStanza(msg);
expect(_converse.log).toHaveBeenCalledWith(
"onMessage: Ignoring incoming message intended for a different resource: romeo@montague.lit/some-other-resource",
Strophe.LogLevel.INFO);
expect(_converse.chatboxes.getChatBox).not.toHaveBeenCalled();
expect(_converse.api.chatboxes.create).not.toHaveBeenCalled();
_converse.filter_by_resource = false;
const message = "This message sent to a different resource will be shown";
......@@ -1884,11 +1880,11 @@
id: '134234623462346'
}).c('body').t(message).up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
await _converse.chatboxes.onMessage(msg);
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => _converse.chatboxviews.keys().length > 1, 1000);
const view = _converse.chatboxviews.get(sender_jid);
await u.waitUntil(() => view.model.messages.length);
expect(_converse.chatboxes.getChatBox).toHaveBeenCalled();
expect(_converse.api.chatboxes.create).toHaveBeenCalled();
const last_message = await u.waitUntil(() => sizzle('.chat-content:last .chat-msg__text', view.el).pop());
const msg_txt = last_message.textContent;
expect(msg_txt).toEqual(message);
......@@ -2121,20 +2117,11 @@
const sent_stanzas = [];
spyOn(_converse.connection, 'send').and.callFake(s => sent_stanzas.push(s));
_converse.connection._dataRecv(test_utils.createRequest(stanza));
await u.waitUntil(() => _converse.api.chats.get().length == 2);
await _converse.handleMessageStanza(stanza);
const sent_messages = sent_stanzas
.map(s => _.isElement(s) ? s : s.nodeTree)
.filter(e => e.nodeName === 'message');
// Only one message is sent out, and it's not a chat marker
expect(sent_messages.length).toBe(1);
expect(Strophe.serialize(sent_messages[0])).toBe(
`<message id="${sent_messages[0].getAttribute('id')}" to="someone@montague.lit" type="chat" xmlns="jabber:client">`+
`<active xmlns="http://jabber.org/protocol/chatstates"/>`+
`<no-store xmlns="urn:xmpp:hints"/>`+
`<no-permanent-store xmlns="urn:xmpp:hints"/>`+
`</message>`);
expect(sent_messages.length).toBe(0);
done();
}));
......
......@@ -95,7 +95,7 @@
id: (new Date()).getTime()
}).c('body').t('This message is sent to a minimized chatbox').up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
_converse.chatboxes.onMessage(msg);
_converse.handleMessageStanza(msg);
}
return u.waitUntil(() => chatview.model.messages.length);
}).then(() => {
......@@ -103,7 +103,7 @@
expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((3).toString());
// Chat state notifications don't increment the unread messages counter
// <composing> state
_converse.chatboxes.onMessage($msg({
_converse.handleMessageStanza($msg({
from: contact_jid,
to: _converse.connection.jid,
type: 'chat',
......@@ -112,7 +112,7 @@
expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((i).toString());
// <paused> state
_converse.chatboxes.onMessage($msg({
_converse.handleMessageStanza($msg({
from: contact_jid,
to: _converse.connection.jid,
type: 'chat',
......@@ -121,7 +121,7 @@
expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((i).toString());
// <gone> state
_converse.chatboxes.onMessage($msg({
_converse.handleMessageStanza($msg({
from: contact_jid,
to: _converse.connection.jid,
type: 'chat',
......@@ -130,7 +130,7 @@
expect(_converse.minimized_chats.toggleview.el.querySelector('.unread-message-count').textContent).toBe((i).toString());
// <inactive> state
_converse.chatboxes.onMessage($msg({
_converse.handleMessageStanza($msg({
from: contact_jid,
to: _converse.connection.jid,
type: 'chat',
......
......@@ -89,7 +89,7 @@
// Non-existing room
muc_jid = 'chillout2@montague.lit';
room = await _converse.api.rooms.get(muc_jid);
expect(typeof room === 'undefined').toBeTruthy();
expect(room).toBe(null);
done();
}));
......
......@@ -120,7 +120,6 @@
by="room@muc.example.com"/>
</message>`);
_converse.connection._dataRecv(test_utils.createRequest(stanza));
await u.waitUntil(() => _converse.api.chats.get().length);
await u.waitUntil(() => view.model.messages.length === 1);
await u.waitUntil(() => view.model.findDuplicateFromStanzaID.calls.count() === 1);
let result = await view.model.findDuplicateFromStanzaID.calls.all()[0].returnValue;
......@@ -572,7 +571,6 @@
const view = _converse.api.chatviews.get(muc_jid);
view.model.sendMessage('hello world');
await u.waitUntil(() => _converse.api.chats.get().length);
await u.waitUntil(() => view.model.messages.length === 1);
const msg = view.model.messages.at(0);
expect(msg.get('stanza_id')).toBeUndefined();
......
......@@ -30,7 +30,7 @@
id: (new Date()).getTime()
}).c('body').t(message).up()
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
await _converse.chatboxes.onMessage(msg); // This will emit 'message'
await _converse.handleMessageStanza(msg); // This will emit 'message'
await u.waitUntil(() => _converse.api.chatviews.get(sender_jid));
expect(_converse.areDesktopNotificationsEnabled).toHaveBeenCalled();
expect(_converse.showMessageNotification).toHaveBeenCalled();
......
......@@ -303,7 +303,6 @@
spyOn(registerview, 'onRegistrationFields').and.callThrough();
spyOn(registerview, 'renderRegistrationForm').and.callThrough();
registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
spyOn(_converse.connection, 'connect').and.callThrough();
registerview.el.querySelector('input[name=domain]').value = 'conversejs.org';
registerview.el.querySelector('input[type=submit]').click();
......
......@@ -263,7 +263,7 @@
expect(window.confirm).toHaveBeenCalledWith(
'Are you sure you want to leave the groupchat lounge@conference.shakespeare.lit?');
await u.waitUntil(() => !_converse.api.rooms.get().length);
await new Promise(resolve => _converse.api.listen.once('chatBoxClosed', resolve));
room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room");
expect(room_els.length).toBe(0);
expect(_converse.chatboxes.length).toBe(1);
......
......@@ -34,8 +34,8 @@
'xmlns': 'urn:xmpp:spoiler:0',
}).t(spoiler_hint)
.tree();
await _converse.chatboxes.onMessage(msg);
await u.waitUntil(() => _converse.api.chats.get().length === 2);
_converse.connection._dataRecv(test_utils.createRequest(msg));
await new Promise(resolve => _converse.api.listen.once('chatBoxInitialized', resolve));
const view = _converse.chatboxviews.get(sender_jid);
await new Promise(resolve => view.once('messageInserted', resolve));
await u.waitUntil(() => view.model.vcard.get('fullname') === 'Mercutio')
......@@ -69,10 +69,10 @@
.c('spoiler', {
'xmlns': 'urn:xmpp:spoiler:0',
}).tree();
await _converse.chatboxes.onMessage(msg);
await u.waitUntil(() => _converse.api.chats.get().length === 2);
_converse.connection._dataRecv(test_utils.createRequest(msg));
await new Promise(resolve => _converse.api.listen.once('chatBoxInitialized', resolve));
const view = _converse.chatboxviews.get(sender_jid);
await new Promise(resolve => view.once('messageInserted', resolve));
await u.waitUntil(() => u.isVisible(view.el));
await u.waitUntil(() => view.model.vcard.get('fullname') === 'Mercutio')
expect(_.includes(view.el.querySelector('.chat-msg__author').textContent, 'Mercutio')).toBeTruthy();
const message_content = view.el.querySelector('.chat-msg__text');
......
......@@ -47,6 +47,7 @@ converse.plugins.add('converse-chatview', {
*/
dependencies: [
"converse-chatboxviews",
"converse-chat",
"converse-disco",
"converse-message-view",
"converse-modal"
......@@ -72,17 +73,6 @@ converse.plugins.add('converse-chatview', {
},
});
function onWindowStateChanged (data) {
if (_converse.chatboxviews) {
_converse.chatboxviews.forEach(view => {
if (view.model.get('id') !== 'controlbox') {
view.onWindowStateChanged(data.state);
}
});
}
}
_converse.api.listen.on('windowStateChanged', onWindowStateChanged);
_converse.ChatBoxHeading = _converse.ViewWithAvatar.extend({
initialize () {
......@@ -92,6 +82,9 @@ converse.plugins.add('converse-chatview', {
if (this.model.vcard) {
this.listenTo(this.model.vcard, 'change', this.debouncedRender);
}
if (this.model.contact) {
this.listenTo(this.model.contact, 'destroy', this.debouncedRender);
}
if (this.model.rosterContactAdded) {
this.model.rosterContactAdded.then(() => {
this.listenTo(this.model.contact, 'change:nickname', this.debouncedRender);
......@@ -101,8 +94,8 @@ converse.plugins.add('converse-chatview', {
},
render () {
const vcard = get(this.model, 'vcard'),
vcard_json = vcard ? vcard.toJSON() : {};
const vcard = get(this.model, 'vcard');
const vcard_json = vcard ? vcard.toJSON() : {};
this.el.innerHTML = tpl_chatbox_head(
Object.assign(
vcard_json,
......@@ -409,10 +402,6 @@ converse.plugins.add('converse-chatview', {
this.heading = new _converse.ChatBoxHeading({'model': this.model});
this.heading.render();
this.heading.chatview = this;
if (this.model.contact !== undefined) {
this.listenTo(this.model.contact, 'destroy', this.heading.render);
}
const flyout = this.el.querySelector('.flyout');
flyout.insertBefore(this.heading.el, flyout.querySelector('.chat-body'));
return this;
......@@ -1299,15 +1288,29 @@ converse.plugins.add('converse-chatview', {
_converse.api.listen.on('chatBoxViewsInitialized', () => {
const views = _converse.chatboxviews;
_converse.chatboxes.on('add', item => {
_converse.chatboxes.on('add', async item => {
if (!views.get(item.get('id')) && item.get('type') === _converse.PRIVATE_CHAT_TYPE) {
await item.initialized;
views.add(item.get('id'), new _converse.ChatBoxView({model: item}));
}
});
});
// Advertise that we support XEP-0382 Message Spoilers
/************************ BEGIN Event Handlers ************************/
function onWindowStateChanged (data) {
if (_converse.chatboxviews) {
_converse.chatboxviews.forEach(view => {
if (view.model.get('id') !== 'controlbox') {
view.onWindowStateChanged(data.state);
}
});
}
}
_converse.api.listen.on('windowStateChanged', onWindowStateChanged);
_converse.api.listen.on('connected', () => _converse.api.disco.own.features.add(Strophe.NS.SPOILER));
/************************ END Event Handlers ************************/
/************************ BEGIN API ************************/
Object.assign(_converse.api, {
......
......@@ -69,7 +69,7 @@ converse.plugins.add('converse-controlbox', {
*
* NB: These plugins need to have already been loaded via require.js.
*/
dependencies: ["converse-modal", "converse-chatboxes", "converse-rosterview", "converse-chatview"],
dependencies: ["converse-modal", "converse-chatboxes", "converse-chat", "converse-rosterview", "converse-chatview"],
enabled (_converse) {
return !_converse.singleton;
......@@ -626,16 +626,25 @@ converse.plugins.add('converse-controlbox', {
* @namespace _converse.api.controlbox
* @memberOf _converse.api
*/
'controlbox': {
controlbox: {
/**
* Retrieves the controlbox view.
*
* Opens the controlbox
* @method _converse.api.controlbox.open
* @returns { Promise<_converse.ControlBox> }
*/
async open () {
await _converse.api.waitUntil('chatBoxesFetched');
const model = await _converse.api.chatboxes.get('controlbox') ||
_converse.api.chatboxes.create('controlbox', {}, _converse.Controlbox);
model.trigger('show');
return model;
},
/**
* Returns the controlbox view.
* @method _converse.api.controlbox.get
*
* @example
* const view = _converse.api.controlbox.get();
*
* @returns {Backbone.View} View representing the controlbox
* @returns { Backbone.View } View representing the controlbox
* @example const view = _converse.api.controlbox.get();
*/
get () {
return _converse.chatboxviews.get('controlbox');
......
// Converse.js (A browser based XMPP chat client)
// https://conversejs.org
//
// Copyright (c) 2019, Jan-Carel Brand <jc@opkode.com>
// Licensed under the Mozilla Public License (MPLv2)
/**
* @module converse-headline
*/
import "converse-chatview";
import converse from "@converse/headless/converse-core";
import tpl_chatbox from "templates/chatbox.html";
converse.plugins.add('converse-headlines-view', {
/* Plugin dependencies are other plugins which might be
* overridden or relied upon, and therefore need to be loaded before
* this plugin.
*
* If the setting "strict_plugin_dependencies" is set to true,
* an error will be raised if the plugin is not found. By default it's
* false, which means these plugins are only loaded opportunistically.
*
* NB: These plugins need to have already been loaded via require.js.
*/
dependencies: ["converse-headlines", "converse-chatview"],
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this;
_converse.HeadlinesBoxView = _converse.ChatBoxView.extend({
className: 'chatbox headlines',
events: {
'click .close-chatbox-button': 'close',
'click .toggle-chatbox-button': 'minimize',
'keypress textarea.chat-textarea': 'onKeyDown'
},
initialize () {
this.initDebounced();
this.model.disable_mam = true; // Don't do MAM queries for this box
this.listenTo(this.model.messages, 'add', this.onMessageAdded);
this.listenTo(this.model, 'show', this.show);
this.listenTo(this.model, 'destroy', this.hide);
this.listenTo(this.model, 'change:minimized', this.onMinimizedChanged);
this.render().insertHeading()
this.updateAfterMessagesFetched();
this.insertIntoDOM().hide();
_converse.api.trigger('chatBoxInitialized', this);
},
render () {
this.el.setAttribute('id', this.model.get('box_id'))
this.el.innerHTML = tpl_chatbox(
Object.assign(this.model.toJSON(), {
info_close: '',
label_personal_message: '',
show_send_button: false,
show_toolbar: false,
unread_msgs: ''
}
));
this.content = this.el.querySelector('.chat-content');
return this;
},
// Override to avoid the methods in converse-chatview.js
'renderMessageForm': function renderMessageForm () {},
'afterShown': function afterShown () {}
});
_converse.api.listen.on('chatBoxViewsInitialized', () => {
const views = _converse.chatboxviews;
_converse.chatboxes.on('add', item => {
if (!views.get(item.get('id')) && item.get('type') === _converse.HEADLINES_TYPE) {
views.add(item.get('id'), new _converse.HeadlinesBoxView({model: item}));
}
});
});
}
});
......@@ -112,8 +112,9 @@ converse.plugins.add('converse-minimize', {
ChatBoxHeading: {
render () {
const { _converse } = this.__super__,
{ __ } = _converse;
const { _converse } = this.__super__;
const { __ } = _converse;
this.__super__.render.apply(this, arguments);
const new_html = tpl_chatbox_minimize({
'info_minimize': __('Minimize this chat box')
......
......@@ -91,7 +91,7 @@ converse.plugins.add('converse-roomslist', {
toHTML () {
return tpl_rooms_list({
'rooms': _converse.api.rooms.get(),
'rooms': this.model.filter(m => m.get('type') === _converse.CHATROOMS_TYPE),
'allow_bookmarks': _converse.allow_bookmarks && _converse.bookmarks,
'collapsed': this.list_model.get('toggle-state') !== _converse.OPENED,
'desc_rooms': __('Click to toggle the list of open groupchats'),
......
......@@ -17,10 +17,10 @@ import "converse-controlbox"; // The control box
import "converse-dragresize"; // Allows chat boxes to be resized by dragging them
import "converse-emoji-views";
import "converse-fullscreen";
import "converse-headline"; // Support for headline messages
import "converse-mam-views";
import "converse-minimize"; // Allows chat boxes to be minimized
import "converse-muc-views"; // Views related to MUC
import "converse-headlines-view";
import "converse-notification"; // HTML5 Notifications
import "converse-omemo";
import "converse-profile";
......@@ -51,6 +51,7 @@ const WHITELISTED_PLUGINS = [
'converse-minimize',
'converse-modal',
'converse-muc-views',
'converse-headlines-view',
'converse-notification',
'converse-omemo',
'converse-profile',
......
import { get, isObject, isString, propertyOf } from "lodash";
import converse from "./converse-core";
import filesize from "filesize";
const { $msg, Backbone, Strophe, dayjs, sizzle, utils } = converse.env;
const u = converse.env.utils;
converse.plugins.add('converse-chat', {
/* Optional dependencies are other plugins which might be
* overridden or relied upon, and therefore need to be loaded before
* this plugin. They are called "optional" because they might not be
* available, in which case any overrides applicable to them will be
* ignored.
*
* It's possible however to make optional dependencies non-optional.
* If the setting "strict_plugin_dependencies" is set to true,
* an error will be raised if the plugin is not found.
*
* NB: These plugins need to have already been loaded via require.js.
*/
dependencies: ["converse-chatboxes", "converse-disco"],
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this;
const { __ } = _converse;
// Configuration values for this plugin
// ====================================
// Refer to docs/source/configuration.rst for explanations of these
// configuration settings.
_converse.api.settings.update({
'auto_join_private_chats': [],
'clear_messages_on_reconnection': false,
'filter_by_resource': false,
'allow_message_corrections': 'all',
'send_chat_state_notifications': true
});
const ModelWithContact = Backbone.Model.extend({
initialize () {
this.rosterContactAdded = u.getResolveablePromise();
},
async setRosterContact (jid) {
const contact = await _converse.api.contacts.get(jid);
if (contact) {
this.contact = contact;
this.set('nickname', contact.get('nickname'));
this.rosterContactAdded.resolve();
}
}
});
/**
* Represents a non-MUC message. These can be either `chat` messages or
* `headline` messages.
* @class
* @namespace _converse.Message
* @memberOf _converse
* @example const msg = new _converse.Message({'message': 'hello world!'});
*/
_converse.Message = ModelWithContact.extend({
defaults () {
return {
'msgid': u.getUniqueId(),
'time': (new Date()).toISOString(),
'ephemeral': false
};
},
async initialize () {
this.initialized = u.getResolveablePromise();
if (this.get('type') === 'chat') {
ModelWithContact.prototype.initialize.apply(this, arguments);
this.setRosterContact(Strophe.getBareJidFromJid(this.get('from')));
}
if (this.get('file')) {
this.on('change:put', this.uploadFile, this);
}
if (this.isEphemeral()) {
window.setTimeout(this.safeDestroy.bind(this), 10000);
}
await _converse.api.trigger('messageInitialized', this, {'Synchronous': true});
this.initialized.resolve();
},
safeDestroy () {
try {
this.destroy()
} catch (e) {
_converse.log(e, Strophe.LogLevel.ERROR);
}
},
isOnlyChatStateNotification () {
return u.isOnlyChatStateNotification(this);
},
isEphemeral () {
return this.isOnlyChatStateNotification() || this.get('ephemeral');
},
getDisplayName () {
if (this.get('type') === 'groupchat') {
return this.get('nick');
} else if (this.contact) {
return this.contact.getDisplayName();
} else if (this.vcard) {
return this.vcard.getDisplayName();
} else {
return this.get('from');
}
},
getMessageText () {
if (this.get('is_encrypted')) {
return this.get('plaintext') ||
(_converse.debug ? __('Unencryptable OMEMO message') : null);
}
return this.get('message');
},
isMeCommand () {
const text = this.getMessageText();
if (!text) {
return false;
}
return text.startsWith('/me ');
},
sendSlotRequestStanza () {
/* Send out an IQ stanza to request a file upload slot.
*
* https://xmpp.org/extensions/xep-0363.html#request
*/
if (!this.file) {
return Promise.reject(new Error("file is undefined"));
}
const iq = converse.env.$iq({
'from': _converse.jid,
'to': this.get('slot_request_url'),
'type': 'get'
}).c('request', {
'xmlns': Strophe.NS.HTTPUPLOAD,
'filename': this.file.name,
'size': this.file.size,
'content-type': this.file.type
})
return _converse.api.sendIQ(iq);
},
async getRequestSlotURL () {
let stanza;
try {
stanza = await this.sendSlotRequestStanza();
} catch (e) {
_converse.log(e, Strophe.LogLevel.ERROR);
return this.save({
'type': 'error',
'message': __("Sorry, could not determine upload URL."),
'ephemeral': true
});
}
const slot = stanza.querySelector('slot');
if (slot) {
this.save({
'get': slot.querySelector('get').getAttribute('url'),
'put': slot.querySelector('put').getAttribute('url'),
});
} else {
return this.save({
'type': 'error',
'message': __("Sorry, could not determine file upload URL."),
'ephemeral': true
});
}
},
uploadFile () {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
_converse.log("Status: " + xhr.status, Strophe.LogLevel.INFO);
if (xhr.status === 200 || xhr.status === 201) {
this.save({
'upload': _converse.SUCCESS,
'oob_url': this.get('get'),
'message': this.get('get')
});
} else {
xhr.onerror();
}
}
};
xhr.upload.addEventListener("progress", (evt) => {
if (evt.lengthComputable) {
this.set('progress', evt.loaded / evt.total);
}
}, false);
xhr.onerror = () => {
let message;
if (xhr.responseText) {
message = __('Sorry, could not succesfully upload your file. Your server’s response: "%1$s"', xhr.responseText)
} else {
message = __('Sorry, could not succesfully upload your file.');
}
this.save({
'type': 'error',
'upload': _converse.FAILURE,
'message': message,
'ephemeral': true
});
};
xhr.open('PUT', this.get('put'), true);
xhr.setRequestHeader("Content-type", this.file.type);
xhr.send(this.file);
}
});
_converse.Messages = _converse.Collection.extend({
model: _converse.Message,
comparator: 'time'
});
/**
* Represents an open/ongoing chat conversation.
*
* @class
* @namespace _converse.ChatBox
* @memberOf _converse
*/
_converse.ChatBox = ModelWithContact.extend({
messagesCollection: _converse.Messages,
defaults () {
return {
'bookmarked': false,
'chat_state': undefined,
'hidden': ['mobile', 'fullscreen'].includes(_converse.view_mode),
'message_type': 'chat',
'nickname': undefined,
'num_unread': 0,
'time_sent': (new Date(0)).toISOString(),
'time_opened': this.get('time_opened') || (new Date()).getTime(),
'type': _converse.PRIVATE_CHAT_TYPE,
'url': ''
}
},
async initialize () {
this.initialized = u.getResolveablePromise();
ModelWithContact.prototype.initialize.apply(this, arguments);
const jid = this.get('jid');
if (!jid) {
// XXX: The `validate` method will prevent this model
// from being persisted if there's no jid, but that gets
// called after model instantiation, so we have to deal
// with invalid models here also.
// This happens when the controlbox is in browser storage,
// but we're in embedded mode.
return;
}
this.set({'box_id': `box-${btoa(jid)}`});
if (_converse.vcards) {
this.vcard = _converse.vcards.findWhere({'jid': jid}) || _converse.vcards.create({'jid': jid});
}
if (this.get('type') === _converse.PRIVATE_CHAT_TYPE) {
this.presence = _converse.presences.findWhere({'jid': jid}) || _converse.presences.create({'jid': jid});
await this.setRosterContact(jid);
}
this.on('change:chat_state', this.sendChatState, this);
this.initMessages();
await this.fetchMessages();
this.initialized.resolve();
},
getMessagesCacheKey () {
return `converse.messages-${this.get('jid')}-${_converse.bare_jid}`;
},
initMessages () {
this.messages = new this.messagesCollection();
this.messages.chatbox = this;
this.messages.browserStorage = _converse.createStore(this.getMessagesCacheKey());
this.listenTo(this.messages, 'change:upload', message => {
if (message.get('upload') === _converse.SUCCESS) {
_converse.api.send(this.createMessageStanza(message));
}
});
},
afterMessagesFetched () {
/**
* Triggered whenever a `_converse.ChatBox` instance has fetched its messages from
* `sessionStorage` but **NOT** from the server.
* @event _converse#afterMessagesFetched
* @type {_converse.ChatBox | _converse.ChatRoom}
* @example _converse.api.listen.on('afterMessagesFetched', view => { ... });
*/
_converse.api.trigger('afterMessagesFetched', this);
},
fetchMessages () {
if (this.messages.fetched) {
_converse.log(`Not re-fetching messages for ${this.get('jid')}`, Strophe.LogLevel.INFO);
return;
}
this.messages.fetched = u.getResolveablePromise();
const resolve = this.messages.fetched.resolve;
this.messages.fetch({
'add': true,
'success': () => { this.afterMessagesFetched(); resolve() },
'error': () => { this.afterMessagesFetched(); resolve() }
});
return this.messages.fetched;
},
async onMessage (stanza, original_stanza, from_jid) {
const message = await this.getDuplicateMessage(stanza);
if (message) {
this.updateMessage(message, original_stanza);
} else {
if (
!this.handleReceipt (stanza, from_jid) &&
!this.handleChatMarker(stanza, from_jid)
) {
const attrs = await this.getMessageAttributesFromStanza(stanza, original_stanza);
this.setEditable(attrs, attrs.time, stanza);
if (attrs['chat_state'] || !u.isEmptyMessage(attrs)) {
const msg = this.correctMessage(attrs) || this.messages.create(attrs);
this.incrementUnreadMsgCounter(msg);
}
}
}
},
async clearMessages () {
try {
await Promise.all(this.messages.models.map(m => m.destroy()));
this.messages.reset();
} catch (e) {
this.messages.trigger('reset');
_converse.log(e, Strophe.LogLevel.ERROR);
} finally {
delete this.messages.fetched;
}
},
async close () {
try {
await new Promise((success, reject) => {
return this.destroy({success, 'error': (m, e) => reject(e)})
});
} catch (e) {
_converse.log(e, Strophe.LogLevel.ERROR);
} finally {
if (_converse.clear_messages_on_reconnection) {
await this.clearMessages();
}
}
},
announceReconnection () {
/**
* Triggered whenever a `_converse.ChatBox` instance has reconnected after an outage
* @event _converse#onChatReconnected
* @type {_converse.ChatBox | _converse.ChatRoom}
* @example _converse.api.listen.on('onChatReconnected', chatbox => { ... });
*/
_converse.api.trigger('chatReconnected', this);
},
onReconnection () {
if (_converse.clear_messages_on_reconnection) {
this.clearMessages();
}
this.announceReconnection();
},
validate (attrs) {
if (!attrs.jid) {
return 'Ignored ChatBox without JID';
}
const room_jids = _converse.auto_join_rooms.map(s => isObject(s) ? s.jid : s);
const auto_join = _converse.auto_join_private_chats.concat(room_jids);
if (_converse.singleton && !auto_join.includes(attrs.jid) && !_converse.auto_join_on_invite) {
const msg = `${attrs.jid} is not allowed because singleton is true and it's not being auto_joined`;
_converse.log(msg, Strophe.LogLevel.WARN);
return msg;
}
},
getDisplayName () {
if (this.contact) {
return this.contact.getDisplayName();
} else if (this.vcard) {
return this.vcard.getDisplayName();
} else {
return this.get('jid');
}
},
createMessageFromError (error) {
if (error instanceof _converse.TimeoutError) {
const msg = this.messages.create({'type': 'error', 'message': error.message, 'retry': true});
msg.error = error;
}
},
getOldestMessage () {
for (let i=0; i<this.messages.length; i++) {
const message = this.messages.at(i);
if (message.get('type') === this.get('message_type')) {
return message;
}
}
},
getMostRecentMessage () {
for (let i=this.messages.length-1; i>=0; i--) {
const message = this.messages.at(i);
if (message.get('type') === this.get('message_type')) {
return message;
}
}
},
getUpdatedMessageAttributes (message, stanza) { // eslint-disable-line no-unused-vars
// Overridden in converse-muc and converse-mam
return {};
},
updateMessage (message, stanza) {
// Overridden in converse-muc and converse-mam
const attrs = this.getUpdatedMessageAttributes(message, stanza);
if (attrs) {
message.save(attrs);
}
},
/**
* Mutator for setting the chat state of this chat session.
* Handles clearing of any chat state notification timeouts and
* setting new ones if necessary.
* Timeouts are set when the state being set is COMPOSING or PAUSED.
* After the timeout, COMPOSING will become PAUSED and PAUSED will become INACTIVE.
* See XEP-0085 Chat State Notifications.
* @private
* @method _converse.ChatBox#setChatState
* @param { string } state - The chat state (consts ACTIVE, COMPOSING, PAUSED, INACTIVE, GONE)
*/
setChatState (state, options) {
if (this.chat_state_timeout !== undefined) {
window.clearTimeout(this.chat_state_timeout);
delete this.chat_state_timeout;
}
if (state === _converse.COMPOSING) {
this.chat_state_timeout = window.setTimeout(
this.setChatState.bind(this),
_converse.TIMEOUTS.PAUSED,
_converse.PAUSED
);
} else if (state === _converse.PAUSED) {
this.chat_state_timeout = window.setTimeout(
this.setChatState.bind(this),
_converse.TIMEOUTS.INACTIVE,
_converse.INACTIVE
);
}
this.set('chat_state', state, options);
return this;
},
/**
* @private
* @method _converse.ChatBox#shouldShowErrorMessage
* @returns {boolean}
*/
shouldShowErrorMessage (stanza) {
const id = stanza.getAttribute('id');
if (id) {
const msgs = this.messages.where({'msgid': id});
const referenced_msgs = msgs.filter(m => m.get('type') !== 'error');
if (!referenced_msgs.length && stanza.querySelector('body') === null) {
// If the error refers to a message not included in our store,
// and it doesn't have a <body> tag, we assume that this was a
// CSI message (which we don't store).
// See https://github.com/conversejs/converse.js/issues/1317
return;
}
const dupes = msgs.filter(m => m.get('type') === 'error');
if (dupes.length) {
return;
}
}
// Gets overridden in ChatRoom
return true;
},
/**
* If the passed in `message` stanza contains an
* [XEP-0308](https://xmpp.org/extensions/xep-0308.html#usecase)
* `<replace>` element, return its `id` attribute.
* @private
* @method _converse.ChatBox#getReplaceId
* @param { XMLElement } stanza
*/
getReplaceId (stanza) {
const el = sizzle(`replace[xmlns="${Strophe.NS.MESSAGE_CORRECT}"]`, stanza).pop();
if (el) {
return el.getAttribute('id');
}
},
/**
* Determine whether the passed in message attributes represent a
* message which corrects a previously received message, or an
* older message which has already been corrected.
* In both cases, update the corrected message accordingly.
* @private
* @method _converse.ChatBox#correctMessage
* @param { object } attrs - Attributes representing a received
* message, as returned by
* {@link _converse.ChatBox.getMessageAttributesFromStanza}
*/
correctMessage (attrs) {
if (!attrs.replaced_id || !attrs.from) {
return;
}
const message = this.messages.findWhere({'msgid': attrs.replaced_id, 'from': attrs.from});
if (!message) {
return;
}
const older_versions = message.get('older_versions') || {};
if ((attrs.time < message.get('time')) && message.get('edited')) {
// This is an older message which has been corrected afterwards
older_versions[attrs.time] = attrs['message'];
message.save({'older_versions': older_versions});
} else {
// This is a correction of an earlier message we already received
older_versions[message.get('time')] = message.get('message');
attrs = Object.assign(attrs, {'older_versions': older_versions});
delete attrs['id']; // Delete id, otherwise a new cache entry gets created
message.save(attrs);
}
return message;
},
async getDuplicateMessage (stanza) {
return this.findDuplicateFromOriginID(stanza) ||
await this.findDuplicateFromStanzaID(stanza) ||
this.findDuplicateFromMessage(stanza);
},
findDuplicateFromOriginID (stanza) {
const origin_id = sizzle(`origin-id[xmlns="${Strophe.NS.SID}"]`, stanza).pop();
if (!origin_id) {
return null;
}
return this.messages.findWhere({
'origin_id': origin_id.getAttribute('id'),
'from': stanza.getAttribute('from')
});
},
async findDuplicateFromStanzaID (stanza) {
const stanza_id = sizzle(`stanza-id[xmlns="${Strophe.NS.SID}"]`, stanza).pop();
if (!stanza_id) {
return false;
}
const by_jid = stanza_id.getAttribute('by');
if (!(await _converse.api.disco.supports(Strophe.NS.SID, by_jid))) {
return false;
}
const query = {};
query[`stanza_id ${by_jid}`] = stanza_id.getAttribute('id');
return this.messages.findWhere(query);
},
findDuplicateFromMessage (stanza) {
const text = this.getMessageBody(stanza) || undefined;
if (!text) { return false; }
const id = stanza.getAttribute('id');
if (!id) { return false; }
return this.messages.findWhere({
'message': text,
'from': stanza.getAttribute('from'),
'msgid': id
});
},
sendMarker(to_jid, id, type) {
const stanza = $msg({
'from': _converse.connection.jid,
'id': u.getUniqueId(),
'to': to_jid,
'type': 'chat',
}).c(type, {'xmlns': Strophe.NS.MARKERS, 'id': id});
_converse.api.send(stanza);
},
handleChatMarker (stanza, from_jid) {
const to_bare_jid = Strophe.getBareJidFromJid(stanza.getAttribute('to'));
if (to_bare_jid !== _converse.bare_jid) {
return false;
}
const markers = sizzle(`[xmlns="${Strophe.NS.MARKERS}"]`, stanza);
if (markers.length === 0) {
return false;
} else if (markers.length > 1) {
_converse.log(
'handleChatMarker: Ignoring incoming stanza with multiple message markers',
Strophe.LogLevel.ERROR
);
_converse.log(stanza, Strophe.LogLevel.ERROR);
return false;
} else {
const marker = markers.pop();
if (marker.nodeName === 'markable') {
if (this.contact && !u.isMAMMessage(stanza) && !u.isCarbonMessage(stanza)) {
this.sendMarker(from_jid, stanza.getAttribute('id'), 'received');
}
return false;
} else {
const msgid = marker && marker.getAttribute('id'),
message = msgid && this.messages.findWhere({msgid}),
field_name = `marker_${marker.nodeName}`;
if (message && !message.get(field_name)) {
message.save({field_name: (new Date()).toISOString()});
}
return true;
}
}
},
sendReceiptStanza (to_jid, id) {
const receipt_stanza = $msg({
'from': _converse.connection.jid,
'id': u.getUniqueId(),
'to': to_jid,
'type': 'chat',
}).c('received', {'xmlns': Strophe.NS.RECEIPTS, 'id': id}).up()
.c('store', {'xmlns': Strophe.NS.HINTS}).up();
_converse.api.send(receipt_stanza);
},
handleReceipt (stanza, from_jid) {
const is_me = Strophe.getBareJidFromJid(from_jid) === _converse.bare_jid;
const requests_receipt = sizzle(`request[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).pop() !== undefined;
if (requests_receipt && !is_me && !u.isCarbonMessage(stanza)) {
this.sendReceiptStanza(from_jid, stanza.getAttribute('id'));
}
const to_bare_jid = Strophe.getBareJidFromJid(stanza.getAttribute('to'));
if (to_bare_jid === _converse.bare_jid) {
const receipt = sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).pop();
if (receipt) {
const msgid = receipt && receipt.getAttribute('id'),
message = msgid && this.messages.findWhere({msgid});
if (message && !message.get('received')) {
message.save({'received': (new Date()).toISOString()});
}
return true;
}
}
return false;
},
/**
* Given a {@link _converse.Message} return the XML stanza that represents it.
* @private
* @method _converse.ChatBox#createMessageStanza
* @param { _converse.Message } message - The message object
*/
createMessageStanza (message) {
const stanza = $msg({
'from': _converse.connection.jid,
'to': this.get('jid'),
'type': this.get('message_type'),
'id': message.get('edited') && u.getUniqueId() || message.get('msgid'),
}).c('body').t(message.get('message')).up()
.c(_converse.ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).root();
if (message.get('type') === 'chat') {
stanza.c('request', {'xmlns': Strophe.NS.RECEIPTS}).root();
}
if (message.get('is_spoiler')) {
if (message.get('spoiler_hint')) {
stanza.c('spoiler', {'xmlns': Strophe.NS.SPOILER}, message.get('spoiler_hint')).root();
} else {
stanza.c('spoiler', {'xmlns': Strophe.NS.SPOILER}).root();
}
}
(message.get('references') || []).forEach(reference => {
const attrs = {
'xmlns': Strophe.NS.REFERENCE,
'begin': reference.begin,
'end': reference.end,
'type': reference.type,
}
if (reference.uri) {
attrs.uri = reference.uri;
}
stanza.c('reference', attrs).root();
});
if (message.get('oob_url')) {
stanza.c('x', {'xmlns': Strophe.NS.OUTOFBAND}).c('url').t(message.get('oob_url')).root();
}
if (message.get('edited')) {
stanza.c('replace', {
'xmlns': Strophe.NS.MESSAGE_CORRECT,
'id': message.get('msgid')
}).root();
}
if (message.get('origin_id')) {
stanza.c('origin-id', {'xmlns': Strophe.NS.SID, 'id': message.get('origin_id')}).root();
}
return stanza;
},
getOutgoingMessageAttributes (text, spoiler_hint) {
const is_spoiler = this.get('composing_spoiler');
const origin_id = u.getUniqueId();
return {
'id': origin_id,
'jid': this.get('jid'),
'nickname': this.get('nickname'),
'msgid': origin_id,
'origin_id': origin_id,
'fullname': _converse.xmppstatus.get('fullname'),
'from': _converse.bare_jid,
'is_single_emoji': text ? u.isSingleEmoji(text) : false,
'sender': 'me',
'time': (new Date()).toISOString(),
'message': text ? u.httpToGeoUri(u.shortnameToUnicode(text), _converse) : undefined,
'is_spoiler': is_spoiler,
'spoiler_hint': is_spoiler ? spoiler_hint : undefined,
'type': this.get('message_type')
}
},
/**
* Responsible for setting the editable attribute of messages.
* If _converse.allow_message_corrections is "last", then only the last
* message sent from me will be editable. If set to "all" all messages
* will be editable. Otherwise no messages will be editable.
* @method _converse.ChatBox#setEditable
* @memberOf _converse.ChatBox
* @param { Object } attrs An object containing message attributes.
* @param { String } send_time - time when the message was sent
*/
setEditable (attrs, send_time, stanza) {
if (stanza && u.isHeadlineMessage(_converse, stanza)) {
return;
}
if (u.isEmptyMessage(attrs) || attrs.sender !== 'me') {
return;
}
if (_converse.allow_message_corrections === 'all') {
attrs.editable = !(attrs.file || 'oob_url' in attrs);
} else if ((_converse.allow_message_corrections === 'last') &&
(send_time > this.get('time_sent'))) {
this.set({'time_sent': send_time});
const msg = this.messages.findWhere({'editable': true});
if (msg) {
msg.save({'editable': false});
}
attrs.editable = !(attrs.file || 'oob_url' in attrs);
}
},
/**
* Responsible for sending off a text message inside an ongoing chat conversation.
* @method _converse.ChatBox#sendMessage
* @memberOf _converse.ChatBox
* @param { String } text - The chat message text
* @param { String } spoiler_hint - An optional hint, if the message being sent is a spoiler
* @returns { _converse.Message }
* @example
* const chat = _converse.api.chats.get('buddy1@example.com');
* chat.sendMessage('hello world');
*/
sendMessage (text, spoiler_hint) {
const attrs = this.getOutgoingMessageAttributes(text, spoiler_hint);
let message = this.messages.findWhere('correcting')
if (message) {
const older_versions = message.get('older_versions') || {};
older_versions[message.get('time')] = message.get('message');
message.save({
'correcting': false,
'edited': (new Date()).toISOString(),
'message': attrs.message,
'older_versions': older_versions,
'references': attrs.references,
'is_single_emoji': attrs.message ? u.isSingleEmoji(attrs.message) : false,
'origin_id': u.getUniqueId(),
'received': undefined
});
} else {
this.setEditable(attrs, (new Date()).toISOString());
message = this.messages.create(attrs);
}
_converse.api.send(this.createMessageStanza(message));
return message;
},
/**
* Sends a message with the current XEP-0085 chat state of the user
* as taken from the `chat_state` attribute of the {@link _converse.ChatBox}.
* @private
* @method _converse.ChatBox#sendChatState
*/
sendChatState () {
if (_converse.send_chat_state_notifications && this.get('chat_state')) {
const allowed = _converse.send_chat_state_notifications;
if (Array.isArray(allowed) && !allowed.includes(this.get('chat_state'))) {
return;
}
_converse.api.send(
$msg({
'id': u.getUniqueId(),
'to': this.get('jid'),
'type': 'chat'
}).c(this.get('chat_state'), {'xmlns': Strophe.NS.CHATSTATES}).up()
.c('no-store', {'xmlns': Strophe.NS.HINTS}).up()
.c('no-permanent-store', {'xmlns': Strophe.NS.HINTS})
);
}
},
async sendFiles (files) {
const result = await _converse.api.disco.features.get(Strophe.NS.HTTPUPLOAD, _converse.domain);
const item = result.pop();
if (!item) {
this.messages.create({
'message': __("Sorry, looks like file upload is not supported by your server."),
'type': 'error',
'ephemeral': true
});
return;
}
const data = item.dataforms.where({'FORM_TYPE': {'value': Strophe.NS.HTTPUPLOAD, 'type': "hidden"}}).pop(),
max_file_size = window.parseInt(get(data, 'attributes.max-file-size.value')),
slot_request_url = get(item, 'id');
if (!slot_request_url) {
this.messages.create({
'message': __("Sorry, looks like file upload is not supported by your server."),
'type': 'error',
'ephemeral': true
});
return;
}
Array.from(files).forEach(file => {
if (!window.isNaN(max_file_size) && window.parseInt(file.size) > max_file_size) {
return this.messages.create({
'message': __('The size of your file, %1$s, exceeds the maximum allowed by your server, which is %2$s.',
file.name, filesize(max_file_size)),
'type': 'error',
'ephemeral': true
});
} else {
const attrs = Object.assign(
this.getOutgoingMessageAttributes(), {
'file': true,
'progress': 0,
'slot_request_url': slot_request_url
});
this.setEditable(attrs, (new Date()).toISOString());
const message = this.messages.create(attrs, {'silent': true});
message.file = file;
this.messages.trigger('add', message);
message.getRequestSlotURL();
}
});
},
getReferencesFromStanza (stanza) {
const text = propertyOf(stanza.querySelector('body'))('textContent');
return sizzle(`reference[xmlns="${Strophe.NS.REFERENCE}"]`, stanza).map(ref => {
const begin = ref.getAttribute('begin'),
end = ref.getAttribute('end');
return {
'begin': begin,
'end': end,
'type': ref.getAttribute('type'),
'value': text.slice(begin, end),
'uri': ref.getAttribute('uri')
};
});
},
/**
* Extract the XEP-0359 stanza IDs from the passed in stanza
* and return a map containing them.
* @private
* @method _converse.ChatBox#getStanzaIDs
* @param { XMLElement } stanza - The message stanza
*/
getStanzaIDs (stanza) {
const attrs = {};
const stanza_ids = sizzle(`stanza-id[xmlns="${Strophe.NS.SID}"]`, stanza);
if (stanza_ids.length) {
stanza_ids.forEach(s => (attrs[`stanza_id ${s.getAttribute('by')}`] = s.getAttribute('id')));
}
const result = sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, stanza).pop();
if (result) {
const by_jid = stanza.getAttribute('from');
attrs[`stanza_id ${by_jid}`] = result.getAttribute('id');
}
const origin_id = sizzle(`origin-id[xmlns="${Strophe.NS.SID}"]`, stanza).pop();
if (origin_id) {
attrs['origin_id'] = origin_id.getAttribute('id');
}
return attrs;
},
isArchived (original_stanza) {
return !!sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop();
},
getErrorMessage (stanza) {
const error = stanza.querySelector('error');
return propertyOf(error.querySelector('text'))('textContent') ||
__('Sorry, an error occurred:') + ' ' + error.innerHTML;
},
/**
* Given a message stanza, return the text contained in its body.
* @private
* @param { XMLElement } stanza
*/
getMessageBody (stanza) {
const type = stanza.getAttribute('type');
if (type === 'error') {
return this.getErrorMessage(stanza);
} else {
const body = stanza.querySelector('body');
if (body) {
return body.textContent.trim();
}
}
},
/**
* Parses a passed in message stanza and returns an object
* of attributes.
* @private
* @method _converse.ChatBox#getMessageAttributesFromStanza
* @param { XMLElement } stanza - The message stanza
* @param { XMLElement } delay - The <delay> node from the stanza, if there was one.
* @param { XMLElement } original_stanza - The original stanza, that contains the
* message stanza, if it was contained, otherwise it's the message stanza itself.
*/
async getMessageAttributesFromStanza (stanza, original_stanza) {
const spoiler = sizzle(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`, original_stanza).pop();
const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop();
const text = this.getMessageBody(stanza) || undefined;
const chat_state = stanza.getElementsByTagName(_converse.COMPOSING).length && _converse.COMPOSING ||
stanza.getElementsByTagName(_converse.PAUSED).length && _converse.PAUSED ||
stanza.getElementsByTagName(_converse.INACTIVE).length && _converse.INACTIVE ||
stanza.getElementsByTagName(_converse.ACTIVE).length && _converse.ACTIVE ||
stanza.getElementsByTagName(_converse.GONE).length && _converse.GONE;
const replaced_id = this.getReplaceId(stanza)
const msgid = replaced_id || stanza.getAttribute('id') || original_stanza.getAttribute('id');
const attrs = Object.assign({
'chat_state': chat_state,
'is_archived': this.isArchived(original_stanza),
'is_delayed': !!delay,
'is_single_emoji': text ? await u.isSingleEmoji(text) : false,
'is_spoiler': !!spoiler,
'message': text,
'msgid': msgid,
'replaced_id': replaced_id,
'references': this.getReferencesFromStanza(stanza),
'subject': propertyOf(stanza.querySelector('subject'))('textContent'),
'thread': propertyOf(stanza.querySelector('thread'))('textContent'),
'time': delay ? dayjs(delay.getAttribute('stamp')).toISOString() : (new Date()).toISOString(),
'type': stanza.getAttribute('type')
}, this.getStanzaIDs(original_stanza));
if (attrs.type === 'groupchat') {
attrs.from = stanza.getAttribute('from');
attrs.nick = Strophe.unescapeNode(Strophe.getResourceFromJid(attrs.from));
attrs.sender = attrs.nick === this.get('nick') ? 'me': 'them';
attrs.received = (new Date()).toISOString();
} else {
attrs.from = Strophe.getBareJidFromJid(stanza.getAttribute('from'));
if (attrs.from === _converse.bare_jid) {
attrs.sender = 'me';
attrs.fullname = _converse.xmppstatus.get('fullname');
} else {
attrs.sender = 'them';
attrs.fullname = this.get('fullname');
}
}
sizzle(`x[xmlns="${Strophe.NS.OUTOFBAND}"]`, stanza).forEach(xform => {
attrs['oob_url'] = xform.querySelector('url').textContent;
attrs['oob_desc'] = xform.querySelector('url').textContent;
});
if (spoiler) {
attrs.spoiler_hint = spoiler.textContent.length > 0 ? spoiler.textContent : '';
}
if (replaced_id) {
attrs['edited'] = (new Date()).toISOString();
}
// We prefer to use one of the XEP-0359 unique and stable stanza IDs as the Model id, to avoid duplicates.
attrs['id'] = attrs['origin_id'] ||
attrs[`stanza_id ${attrs.from}`] ||
u.getUniqueId();
return attrs;
},
maybeShow () {
// Returns the chatbox
return this.trigger("show");
},
/**
* Indicates whether the chat is hidden and therefore
* whether a newly received message will be visible
* to the user or not.
* @returns {boolean}
*/
isHidden () {
return this.get('hidden') ||
this.get('minimized') ||
this.isScrolledUp() ||
_converse.windowState === 'hidden';
},
/**
* Given a newly received {@link _converse.Message} instance,
* update the unread counter if necessary.
* @private
* @param {_converse.Message} message
*/
incrementUnreadMsgCounter (message) {
if (!message || !message.get('message')) {
return;
}
if (utils.isNewMessage(message) && this.isHidden()) {
this.save({'num_unread': this.get('num_unread') + 1});
_converse.incrementMsgCounter();
}
},
clearUnreadMsgCounter () {
u.safeSave(this, {'num_unread': 0});
},
isScrolledUp () {
return this.get('scrolled', true);
}
});
function rejectMessage (stanza, text) {
// Reject an incoming message by replying with an error message of type "cancel".
_converse.api.send(
$msg({
'to': stanza.getAttribute('from'),
'type': 'error',
'id': stanza.getAttribute('id')
}).c('error', {'type': 'cancel'})
.c('not-allowed', {xmlns:"urn:ietf:params:xml:ns:xmpp-stanzas"}).up()
.c('text', {xmlns:"urn:ietf:params:xml:ns:xmpp-stanzas"}).t(text)
);
_converse.log(`Rejecting message stanza with the following reason: ${text}`, Strophe.LogLevel.WARN);
_converse.log(stanza, Strophe.LogLevel.WARN);
}
async function handleErrorMessage (stanza) {
const from_jid = Strophe.getBareJidFromJid(stanza.getAttribute('from'));
if (utils.isSameBareJID(from_jid, _converse.bare_jid)) {
return;
}
const chatbox = await _converse.api.chatboxes.get(from_jid);
if (!chatbox) {
return;
}
const should_show = await chatbox.shouldShowErrorMessage(stanza);
if (!should_show) {
return;
}
const attrs = await chatbox.getMessageAttributesFromStanza(stanza, stanza);
await chatbox.messages.create(attrs);
}
/**
* Handler method for all incoming single-user chat "message" stanzas.
* @private
* @method _converse#handleMessageStanza
* @param { XMLElement } stanza - The incoming message stanza
*/
_converse.handleMessageStanza = async function (stanza) {
const original_stanza = stanza;
let to_jid = stanza.getAttribute('to');
const to_resource = Strophe.getResourceFromJid(to_jid);
if (_converse.filter_by_resource && (to_resource && to_resource !== _converse.resource)) {
return _converse.log(
`onMessage: Ignoring incoming message intended for a different resource: ${to_jid}`,
Strophe.LogLevel.INFO
);
} else if (utils.isHeadlineMessage(_converse, stanza)) {
// XXX: Prosody sends headline messages with the
// wrong type ('chat'), so we need to filter them out here.
return _converse.log(
`onMessage: Ignoring incoming headline message from JID: ${stanza.getAttribute('from')}`,
Strophe.LogLevel.INFO
);
}
const bare_forward = sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).length;
if (bare_forward) {
return rejectMessage(
stanza,
'Forwarded messages not part of an encapsulating protocol are not supported'
);
}
let from_jid = stanza.getAttribute('from') || _converse.bare_jid;
if (u.isCarbonMessage(stanza)) {
if (from_jid === _converse.bare_jid) {
const selector = `[xmlns="${Strophe.NS.CARBONS}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`;
stanza = sizzle(selector, stanza).pop();
to_jid = stanza.getAttribute('to');
from_jid = stanza.getAttribute('from');
} else {
// Prevent message forging via carbons: https://xmpp.org/extensions/xep-0280.html#security
return rejectMessage(stanza, 'Rejecting carbon from invalid JID');
}
}
if (u.isMAMMessage(stanza)) {
if (from_jid === _converse.bare_jid) {
const selector = `[xmlns="${Strophe.NS.MAM}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`;
stanza = sizzle(selector, stanza).pop();
to_jid = stanza.getAttribute('to');
from_jid = stanza.getAttribute('from');
} else {
return _converse.log(
`onMessage: Ignoring alleged MAM message from ${stanza.getAttribute('from')}`,
Strophe.LogLevel.WARN
);
}
}
const from_bare_jid = Strophe.getBareJidFromJid(from_jid);
const is_me = from_bare_jid === _converse.bare_jid;
if (is_me && to_jid === null) {
return _converse.log(
`Don't know how to handle message stanza without 'to' attribute. ${stanza.outerHTML}`,
Strophe.LogLevel.ERROR
);
}
const contact_jid = is_me ? Strophe.getBareJidFromJid(to_jid) : from_bare_jid;
const contact = await _converse.api.contacts.get(contact_jid);
if (contact === undefined && !_converse.allow_non_roster_messaging) {
_converse.log(
`Blocking messaging with a JID not in our roster because allow_non_roster_messaging is false.`,
Strophe.LogLevel.ERROR
);
return _converse.log(stanza, Strophe.LogLevel.ERROR);
}
// Get chat box, but only create when the message has something to show to the user
const has_body = sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).length > 0;
const roster_nick = get(contact, 'attributes.nickname');
const chatbox = await _converse.api.chats.get(contact_jid, {'nickname': roster_nick}, has_body);
chatbox && await chatbox.onMessage(stanza, original_stanza, from_jid);
/**
* Triggered when a message stanza is been received and processed.
* @event _converse#message
* @type { object }
* @property { _converse.ChatBox | _converse.ChatRoom } chatbox
* @property { XMLElement } stanza
* @example _converse.api.listen.on('message', obj => { ... });
*/
_converse.api.trigger('message', {'stanza': original_stanza, 'chatbox': chatbox});
}
function registerMessageHandlers () {
_converse.connection.addHandler(stanza => {
if (sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, stanza).pop()) {
// MAM messages are handled in converse-mam.
// We shouldn't get MAM messages here because
// they shouldn't have a `type` attribute.
_converse.log(`Received a MAM message with type "chat".`, Strophe.LogLevel.WARN);
return true;
}
_converse.handleMessageStanza(stanza);
return true;
}, null, 'message', 'chat');
_converse.connection.addHandler(stanza => {
// Message receipts are usually without the `type` attribute. See #1353
if (stanza.getAttribute('type') !== null) {
// TODO: currently Strophe has no way to register a handler
// for stanzas without a `type` attribute.
// We could update it to accept null to mean no attribute,
// but that would be a backward-incompatible change
return true; // Gets handled above.
}
_converse.handleMessageStanza(stanza);
return true;
}, Strophe.NS.RECEIPTS, 'message');
_converse.connection.addHandler(stanza => {
handleErrorMessage(stanza);
return true;
}, null, 'message', 'error');
}
function autoJoinChats () {
// Automatically join private chats, based on the
// "auto_join_private_chats" configuration setting.
_converse.auto_join_private_chats.forEach(jid => {
if (_converse.chatboxes.where({'jid': jid}).length) {
return;
}
if (isString(jid)) {
_converse.api.chats.open(jid);
} else {
_converse.log(
'Invalid jid criteria specified for "auto_join_private_chats"',
Strophe.LogLevel.ERROR);
}
});
/**
* Triggered once any private chats have been automatically joined as
* specified by the `auto_join_private_chats` setting.
* See: https://conversejs.org/docs/html/configuration.html#auto-join-private-chats
* @event _converse#privateChatsAutoJoined
* @example _converse.api.listen.on('privateChatsAutoJoined', () => { ... });
* @example _converse.api.waitUntil('privateChatsAutoJoined').then(() => { ... });
*/
_converse.api.trigger('privateChatsAutoJoined');
}
/************************ BEGIN Route Handlers ************************/
function openChat (jid) {
if (!utils.isValidJID(jid)) {
return _converse.log(
`Invalid JID "${jid}" provided in URL fragment`,
Strophe.LogLevel.WARN
);
}
_converse.api.chats.open(jid);
}
_converse.router.route('converse/chat?jid=:jid', openChat);
/************************ END Route Handlers ************************/
/************************ BEGIN Event Handlers ************************/
_converse.api.listen.on('chatBoxesFetched', autoJoinChats);
_converse.api.listen.on('presencesInitialized', (reconnecting) => (!reconnecting && registerMessageHandlers()));
_converse.api.listen.on('clearSession', () => {
if (_converse.shouldClearCache()) {
_converse.chatboxes.filter(c => c.messages && c.messages.clearSession({'silent': true}));
}
});
/************************ END Event Handlers ************************/
/************************ BEGIN API ************************/
Object.assign(_converse.api, {
/**
* The "chats" namespace (used for one-on-one chats)
*
* @namespace _converse.api.chats
* @memberOf _converse.api
*/
chats: {
/**
* @method _converse.api.chats.create
* @param {string|string[]} jid|jids An jid or array of jids
* @param {object} [attrs] An object containing configuration attributes.
*/
async create (jids, attrs) {
if (isString(jids)) {
if (attrs && !get(attrs, 'fullname')) {
const contact = await _converse.api.contacts.get(jids);
attrs.fullname = get(contact, 'attributes.fullname');
}
const chatbox = _converse.api.chats.get(jids, attrs, true);
if (!chatbox) {
_converse.log("Could not open chatbox for JID: "+jids, Strophe.LogLevel.ERROR);
return;
}
return chatbox;
}
if (Array.isArray(jids)) {
return Promise.all(jids.forEach(async jid => {
const contact = await _converse.api.contacts.get(jids);
attrs.fullname = get(contact, 'attributes.fullname');
return _converse.api.chats.get(jid, attrs, true).maybeShow();
}));
}
_converse.log(
"chats.create: You need to provide at least one JID",
Strophe.LogLevel.ERROR
);
return null;
},
/**
* Opens a new one-on-one chat.
*
* @method _converse.api.chats.open
* @param {String|string[]} name - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
* @param {Object} [attrs] - Attributes to be set on the _converse.ChatBox model.
* @param {Boolean} [attrs.minimized] - Should the chat be created in minimized state.
* @param {Boolean} [force=false] - By default, a minimized
* chat won't be maximized (in `overlayed` view mode) and in
* `fullscreen` view mode a newly opened chat won't replace
* another chat already in the foreground.
* Set `force` to `true` if you want to force the chat to be
* maximized or shown.
* @returns {Promise} Promise which resolves with the
* _converse.ChatBox representing the chat.
*
* @example
* // To open a single chat, provide the JID of the contact you're chatting with in that chat:
* converse.plugins.add('myplugin', {
* initialize: function() {
* const _converse = this._converse;
* // Note, buddy@example.org must be in your contacts roster!
* _converse.api.chats.open('buddy@example.com').then(chat => {
* // Now you can do something with the chat model
* });
* }
* });
*
* @example
* // To open an array of chats, provide an array of JIDs:
* converse.plugins.add('myplugin', {
* initialize: function () {
* const _converse = this._converse;
* // Note, these users must first be in your contacts roster!
* _converse.api.chats.open(['buddy1@example.com', 'buddy2@example.com']).then(chats => {
* // Now you can do something with the chat models
* });
* }
* });
*/
async open (jids, attrs, force) {
if (isString(jids)) {
const chat = await _converse.api.chats.get(jids, attrs, true);
if (chat) {
return chat.maybeShow(force);
}
return chat;
} else if (Array.isArray(jids)) {
return Promise.all(
jids.map(j => _converse.api.chats.get(j, attrs, true).then(c => c && c.maybeShow(force)))
.filter(c => c)
);
}
const err_msg = "chats.open: You need to provide at least one JID";
_converse.log(err_msg, Strophe.LogLevel.ERROR);
throw new Error(err_msg);
},
/**
* Retrieves a chat or all chats.
*
* @method _converse.api.chats.get
* @param {String|string[]} jids - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
* @param {Object} [attrs] - Attributes to be set on the _converse.ChatBox model.
* @param {Boolean} [create=false] - Whether the chat should be created if it's not found.
* @returns { Promise<_converse.ChatBox> }
*
* @example
* // To return a single chat, provide the JID of the contact you're chatting with in that chat:
* const model = await _converse.api.chats.get('buddy@example.com');
*
* @example
* // To return an array of chats, provide an array of JIDs:
* const models = await _converse.api.chats.get(['buddy1@example.com', 'buddy2@example.com']);
*
* @example
* // To return all open chats, call the method without any parameters::
* const models = await _converse.api.chats.get();
*
*/
async get (jids, attrs={}, create=false) {
async function _get (jid) {
let model = await _converse.api.chatboxes.get(jid);
if (!model && create) {
model = await _converse.api.chatboxes.create(jid, attrs, _converse.ChatBox);
} else {
model = (model && model.get('type') === _converse.PRIVATE_CHAT_TYPE) ? model : null;
if (model && Object.keys(attrs).length) {
model.save(attrs);
}
}
return model;
}
if (jids === undefined) {
const chats = await _converse.api.chatboxes.get();
return chats.filter(c => (c.get('type') === _converse.PRIVATE_CHAT_TYPE));
} else if (isString(jids)) {
return _get(jids);
}
return Promise.all(jids.map(jid => _get(jid)));
}
}
});
/************************ END API ************************/
}
});
......@@ -7,13 +7,10 @@
* @module converse-chatboxes
*/
import "./converse-emoji";
import "./utils/form";
import { get, isObject, isString, propertyOf } from "lodash";
import converse from "./converse-core";
import filesize from "filesize";
import { isString } from "lodash";
const { $msg, Backbone, Strophe, dayjs, sizzle, utils } = converse.env;
const u = converse.env.utils;
const { Strophe } = converse.env;
Strophe.addNamespace('MESSAGE_CORRECT', 'urn:xmpp:message-correct:0');
Strophe.addNamespace('RECEIPTS', 'urn:xmpp:receipts');
......@@ -29,20 +26,8 @@ converse.plugins.add('converse-chatboxes', {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this,
{ __ } = _converse;
const { _converse } = this;
// Configuration values for this plugin
// ====================================
// Refer to docs/source/configuration.rst for explanations of these
// configuration settings.
_converse.api.settings.update({
'auto_join_private_chats': [],
'clear_messages_on_reconnection': false,
'filter_by_resource': false,
'allow_message_corrections': 'all',
'send_chat_state_notifications': true
});
_converse.api.promises.add([
'chatBoxesFetched',
'chatBoxesInitialized',
......@@ -76,1032 +61,6 @@ converse.plugins.add('converse-chatboxes', {
};
function openChat (jid) {
if (!utils.isValidJID(jid)) {
return _converse.log(
`Invalid JID "${jid}" provided in URL fragment`,
Strophe.LogLevel.WARN
);
}
_converse.api.chats.open(jid);
}
_converse.router.route('converse/chat?jid=:jid', openChat);
const ModelWithContact = Backbone.Model.extend({
initialize () {
this.rosterContactAdded = u.getResolveablePromise();
},
async setRosterContact (jid) {
const contact = await _converse.api.contacts.get(jid);
if (contact) {
this.contact = contact;
this.set('nickname', contact.get('nickname'));
this.rosterContactAdded.resolve();
}
}
});
/**
* Represents a non-MUC message. These can be either `chat` messages or
* `headline` messages.
* @class
* @namespace _converse.Message
* @memberOf _converse
* @example const msg = new _converse.Message({'message': 'hello world!'});
*/
_converse.Message = ModelWithContact.extend({
defaults () {
return {
'msgid': u.getUniqueId(),
'time': (new Date()).toISOString(),
'ephemeral': false
};
},
async initialize () {
this.initialized = u.getResolveablePromise();
ModelWithContact.prototype.initialize.apply(this, arguments);
if (this.get('type') === 'chat') {
this.setRosterContact(Strophe.getBareJidFromJid(this.get('from')));
}
if (this.get('file')) {
this.on('change:put', this.uploadFile, this);
}
if (this.isEphemeral()) {
window.setTimeout(this.safeDestroy.bind(this), 10000);
}
await _converse.api.trigger('messageInitialized', this, {'Synchronous': true});
this.initialized.resolve();
},
safeDestroy () {
try {
this.destroy()
} catch (e) {
_converse.log(e, Strophe.LogLevel.ERROR);
}
},
isOnlyChatStateNotification () {
return u.isOnlyChatStateNotification(this);
},
isEphemeral () {
return this.isOnlyChatStateNotification() || this.get('ephemeral');
},
getDisplayName () {
if (this.get('type') === 'groupchat') {
return this.get('nick');
} else if (this.contact) {
return this.contact.getDisplayName();
} else if (this.vcard) {
return this.vcard.getDisplayName();
} else {
return this.get('from');
}
},
getMessageText () {
if (this.get('is_encrypted')) {
return this.get('plaintext') ||
(_converse.debug ? __('Unencryptable OMEMO message') : null);
}
return this.get('message');
},
isMeCommand () {
const text = this.getMessageText();
if (!text) {
return false;
}
return text.startsWith('/me ');
},
sendSlotRequestStanza () {
/* Send out an IQ stanza to request a file upload slot.
*
* https://xmpp.org/extensions/xep-0363.html#request
*/
if (!this.file) {
return Promise.reject(new Error("file is undefined"));
}
const iq = converse.env.$iq({
'from': _converse.jid,
'to': this.get('slot_request_url'),
'type': 'get'
}).c('request', {
'xmlns': Strophe.NS.HTTPUPLOAD,
'filename': this.file.name,
'size': this.file.size,
'content-type': this.file.type
})
return _converse.api.sendIQ(iq);
},
async getRequestSlotURL () {
let stanza;
try {
stanza = await this.sendSlotRequestStanza();
} catch (e) {
_converse.log(e, Strophe.LogLevel.ERROR);
return this.save({
'type': 'error',
'message': __("Sorry, could not determine upload URL."),
'ephemeral': true
});
}
const slot = stanza.querySelector('slot');
if (slot) {
this.save({
'get': slot.querySelector('get').getAttribute('url'),
'put': slot.querySelector('put').getAttribute('url'),
});
} else {
return this.save({
'type': 'error',
'message': __("Sorry, could not determine file upload URL."),
'ephemeral': true
});
}
},
uploadFile () {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
_converse.log("Status: " + xhr.status, Strophe.LogLevel.INFO);
if (xhr.status === 200 || xhr.status === 201) {
this.save({
'upload': _converse.SUCCESS,
'oob_url': this.get('get'),
'message': this.get('get')
});
} else {
xhr.onerror();
}
}
};
xhr.upload.addEventListener("progress", (evt) => {
if (evt.lengthComputable) {
this.set('progress', evt.loaded / evt.total);
}
}, false);
xhr.onerror = () => {
let message;
if (xhr.responseText) {
message = __('Sorry, could not succesfully upload your file. Your server’s response: "%1$s"', xhr.responseText)
} else {
message = __('Sorry, could not succesfully upload your file.');
}
this.save({
'type': 'error',
'upload': _converse.FAILURE,
'message': message,
'ephemeral': true
});
};
xhr.open('PUT', this.get('put'), true);
xhr.setRequestHeader("Content-type", this.file.type);
xhr.send(this.file);
}
});
_converse.Messages = _converse.Collection.extend({
model: _converse.Message,
comparator: 'time'
});
/**
* Represents an open/ongoing chat conversation.
*
* @class
* @namespace _converse.ChatBox
* @memberOf _converse
*/
_converse.ChatBox = ModelWithContact.extend({
messagesCollection: _converse.Messages,
defaults () {
return {
'bookmarked': false,
'chat_state': undefined,
'hidden': ['mobile', 'fullscreen'].includes(_converse.view_mode),
'message_type': 'chat',
'nickname': undefined,
'num_unread': 0,
'time_sent': (new Date(0)).toISOString(),
'time_opened': this.get('time_opened') || (new Date()).getTime(),
'type': _converse.PRIVATE_CHAT_TYPE,
'url': ''
}
},
initialize () {
ModelWithContact.prototype.initialize.apply(this, arguments);
const jid = this.get('jid');
if (!jid) {
// XXX: The `validate` method will prevent this model
// from being persisted if there's no jid, but that gets
// called after model instantiation, so we have to deal
// with invalid models here also.
// This happens when the controlbox is in browser storage,
// but we're in embedded mode.
return;
}
this.set({'box_id': `box-${btoa(jid)}`});
if (_converse.vcards) {
this.vcard = _converse.vcards.findWhere({'jid': jid}) || _converse.vcards.create({'jid': jid});
}
if (this.get('type') === _converse.PRIVATE_CHAT_TYPE) {
this.presence = _converse.presences.findWhere({'jid': jid}) || _converse.presences.create({'jid': jid});
this.setRosterContact(jid);
}
this.on('change:chat_state', this.sendChatState, this);
this.initMessages();
this.fetchMessages();
},
getMessagesCacheKey () {
return `converse.messages-${this.get('jid')}-${_converse.bare_jid}`;
},
initMessages () {
this.messages = new this.messagesCollection();
this.messages.chatbox = this;
this.messages.browserStorage = _converse.createStore(this.getMessagesCacheKey());
this.listenTo(this.messages, 'change:upload', message => {
if (message.get('upload') === _converse.SUCCESS) {
_converse.api.send(this.createMessageStanza(message));
}
});
},
afterMessagesFetched () {
/**
* Triggered whenever a `_converse.ChatBox` instance has fetched its messages from
* `sessionStorage` but **NOT** from the server.
* @event _converse#afterMessagesFetched
* @type {_converse.ChatBox | _converse.ChatRoom}
* @example _converse.api.listen.on('afterMessagesFetched', view => { ... });
*/
_converse.api.trigger('afterMessagesFetched', this);
},
fetchMessages () {
if (this.messages.fetched) {
_converse.log(`Not re-fetching messages for ${this.get('jid')}`, Strophe.LogLevel.INFO);
return;
}
this.messages.fetched = u.getResolveablePromise();
const resolve = this.messages.fetched.resolve;
this.messages.fetch({
'add': true,
'success': () => { this.afterMessagesFetched(); resolve() },
'error': () => { this.afterMessagesFetched(); resolve() }
});
return this.messages.fetched;
},
async clearMessages () {
try {
await Promise.all(this.messages.models.map(m => m.destroy()));
this.messages.reset();
} catch (e) {
this.messages.trigger('reset');
_converse.log(e, Strophe.LogLevel.ERROR);
} finally {
delete this.messages.fetched;
}
},
async close () {
try {
await new Promise((success, reject) => {
return this.destroy({success, 'error': (m, e) => reject(e)})
});
} catch (e) {
_converse.log(e, Strophe.LogLevel.ERROR);
} finally {
if (_converse.clear_messages_on_reconnection) {
await this.clearMessages();
}
}
},
announceReconnection () {
/**
* Triggered whenever a `_converse.ChatBox` instance has reconnected after an outage
* @event _converse#onChatReconnected
* @type {_converse.ChatBox | _converse.ChatRoom}
* @example _converse.api.listen.on('onChatReconnected', chatbox => { ... });
*/
_converse.api.trigger('chatReconnected', this);
},
onReconnection () {
if (_converse.clear_messages_on_reconnection) {
this.clearMessages();
}
this.announceReconnection();
},
validate (attrs) {
if (!attrs.jid) {
return 'Ignored ChatBox without JID';
}
const room_jids = _converse.auto_join_rooms.map(s => isObject(s) ? s.jid : s);
const auto_join = _converse.auto_join_private_chats.concat(room_jids);
if (_converse.singleton && !auto_join.includes(attrs.jid) && !_converse.auto_join_on_invite) {
const msg = `${attrs.jid} is not allowed because singleton is true and it's not being auto_joined`;
_converse.log(msg, Strophe.LogLevel.WARN);
return msg;
}
},
getDisplayName () {
if (this.contact) {
return this.contact.getDisplayName();
} else if (this.vcard) {
return this.vcard.getDisplayName();
} else {
return this.get('jid');
}
},
createMessageFromError (error) {
if (error instanceof _converse.TimeoutError) {
const msg = this.messages.create({'type': 'error', 'message': error.message, 'retry': true});
msg.error = error;
}
},
getOldestMessage () {
for (let i=0; i<this.messages.length; i++) {
const message = this.messages.at(i);
if (message.get('type') === this.get('message_type')) {
return message;
}
}
},
getMostRecentMessage () {
for (let i=this.messages.length-1; i>=0; i--) {
const message = this.messages.at(i);
if (message.get('type') === this.get('message_type')) {
return message;
}
}
},
getUpdatedMessageAttributes (message, stanza) { // eslint-disable-line no-unused-vars
// Overridden in converse-muc and converse-mam
return {};
},
updateMessage (message, stanza) {
// Overridden in converse-muc and converse-mam
const attrs = this.getUpdatedMessageAttributes(message, stanza);
if (attrs) {
message.save(attrs);
}
},
/**
* Mutator for setting the chat state of this chat session.
* Handles clearing of any chat state notification timeouts and
* setting new ones if necessary.
* Timeouts are set when the state being set is COMPOSING or PAUSED.
* After the timeout, COMPOSING will become PAUSED and PAUSED will become INACTIVE.
* See XEP-0085 Chat State Notifications.
* @private
* @method _converse.ChatBox#setChatState
* @param { string } state - The chat state (consts ACTIVE, COMPOSING, PAUSED, INACTIVE, GONE)
*/
setChatState (state, options) {
if (this.chat_state_timeout !== undefined) {
window.clearTimeout(this.chat_state_timeout);
delete this.chat_state_timeout;
}
if (state === _converse.COMPOSING) {
this.chat_state_timeout = window.setTimeout(
this.setChatState.bind(this),
_converse.TIMEOUTS.PAUSED,
_converse.PAUSED
);
} else if (state === _converse.PAUSED) {
this.chat_state_timeout = window.setTimeout(
this.setChatState.bind(this),
_converse.TIMEOUTS.INACTIVE,
_converse.INACTIVE
);
}
this.set('chat_state', state, options);
return this;
},
/**
* @private
* @method _converse.ChatBox#shouldShowErrorMessage
* @returns {boolean}
*/
shouldShowErrorMessage (stanza) {
const id = stanza.getAttribute('id');
if (id) {
const msgs = this.messages.where({'msgid': id});
const referenced_msgs = msgs.filter(m => m.get('type') !== 'error');
if (!referenced_msgs.length && stanza.querySelector('body') === null) {
// If the error refers to a message not included in our store,
// and it doesn't have a <body> tag, we assume that this was a
// CSI message (which we don't store).
// See https://github.com/conversejs/converse.js/issues/1317
return;
}
const dupes = msgs.filter(m => m.get('type') === 'error');
if (dupes.length) {
return;
}
}
// Gets overridden in ChatRoom
return true;
},
/**
* If the passed in `message` stanza contains an
* [XEP-0308](https://xmpp.org/extensions/xep-0308.html#usecase)
* `<replace>` element, return its `id` attribute.
* @private
* @method _converse.ChatBox#getReplaceId
* @param { XMLElement } stanza
*/
getReplaceId (stanza) {
const el = sizzle(`replace[xmlns="${Strophe.NS.MESSAGE_CORRECT}"]`, stanza).pop();
if (el) {
return el.getAttribute('id');
}
},
/**
* Determine whether the passed in message attributes represent a
* message which corrects a previously received message, or an
* older message which has already been corrected.
* In both cases, update the corrected message accordingly.
* @private
* @method _converse.ChatBox#correctMessage
* @param { object } attrs - Attributes representing a received
* message, as returned by
* {@link _converse.ChatBox.getMessageAttributesFromStanza}
*/
correctMessage (attrs) {
if (!attrs.replaced_id || !attrs.from) {
return;
}
const message = this.messages.findWhere({'msgid': attrs.replaced_id, 'from': attrs.from});
if (!message) {
return;
}
const older_versions = message.get('older_versions') || {};
if ((attrs.time < message.get('time')) && message.get('edited')) {
// This is an older message which has been corrected afterwards
older_versions[attrs.time] = attrs['message'];
message.save({'older_versions': older_versions});
} else {
// This is a correction of an earlier message we already received
older_versions[message.get('time')] = message.get('message');
attrs = Object.assign(attrs, {'older_versions': older_versions});
delete attrs['id']; // Delete id, otherwise a new cache entry gets created
message.save(attrs);
}
return message;
},
async getDuplicateMessage (stanza) {
return this.findDuplicateFromOriginID(stanza) ||
await this.findDuplicateFromStanzaID(stanza) ||
this.findDuplicateFromMessage(stanza);
},
findDuplicateFromOriginID (stanza) {
const origin_id = sizzle(`origin-id[xmlns="${Strophe.NS.SID}"]`, stanza).pop();
if (!origin_id) {
return null;
}
return this.messages.findWhere({
'origin_id': origin_id.getAttribute('id'),
'from': stanza.getAttribute('from')
});
},
async findDuplicateFromStanzaID (stanza) {
const stanza_id = sizzle(`stanza-id[xmlns="${Strophe.NS.SID}"]`, stanza).pop();
if (!stanza_id) {
return false;
}
const by_jid = stanza_id.getAttribute('by');
if (!(await _converse.api.disco.supports(Strophe.NS.SID, by_jid))) {
return false;
}
const query = {};
query[`stanza_id ${by_jid}`] = stanza_id.getAttribute('id');
return this.messages.findWhere(query);
},
findDuplicateFromMessage (stanza) {
const text = this.getMessageBody(stanza) || undefined;
if (!text) { return false; }
const id = stanza.getAttribute('id');
if (!id) { return false; }
return this.messages.findWhere({
'message': text,
'from': stanza.getAttribute('from'),
'msgid': id
});
},
sendMarker(to_jid, id, type) {
const stanza = $msg({
'from': _converse.connection.jid,
'id': u.getUniqueId(),
'to': to_jid,
'type': 'chat',
}).c(type, {'xmlns': Strophe.NS.MARKERS, 'id': id});
_converse.api.send(stanza);
},
handleChatMarker (stanza, from_jid, is_carbon, is_roster_contact, is_mam) {
const to_bare_jid = Strophe.getBareJidFromJid(stanza.getAttribute('to'));
if (to_bare_jid !== _converse.bare_jid) {
return false;
}
const markers = sizzle(`[xmlns="${Strophe.NS.MARKERS}"]`, stanza);
if (markers.length === 0) {
return false;
} else if (markers.length > 1) {
_converse.log(
'handleChatMarker: Ignoring incoming stanza with multiple message markers',
Strophe.LogLevel.ERROR
);
_converse.log(stanza, Strophe.LogLevel.ERROR);
return false;
} else {
const marker = markers.pop();
if (marker.nodeName === 'markable') {
if (is_roster_contact && !is_carbon && !is_mam) {
this.sendMarker(from_jid, stanza.getAttribute('id'), 'received');
}
return false;
} else {
const msgid = marker && marker.getAttribute('id'),
message = msgid && this.messages.findWhere({msgid}),
field_name = `marker_${marker.nodeName}`;
if (message && !message.get(field_name)) {
message.save({field_name: (new Date()).toISOString()});
}
return true;
}
}
},
sendReceiptStanza (to_jid, id) {
const receipt_stanza = $msg({
'from': _converse.connection.jid,
'id': u.getUniqueId(),
'to': to_jid,
'type': 'chat',
}).c('received', {'xmlns': Strophe.NS.RECEIPTS, 'id': id}).up()
.c('store', {'xmlns': Strophe.NS.HINTS}).up();
_converse.api.send(receipt_stanza);
},
handleReceipt (stanza, from_jid, is_carbon, is_me) {
const requests_receipt = sizzle(`request[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).pop() !== undefined;
if (requests_receipt && !is_carbon && !is_me) {
this.sendReceiptStanza(from_jid, stanza.getAttribute('id'));
}
const to_bare_jid = Strophe.getBareJidFromJid(stanza.getAttribute('to'));
if (to_bare_jid === _converse.bare_jid) {
const receipt = sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).pop();
if (receipt) {
const msgid = receipt && receipt.getAttribute('id'),
message = msgid && this.messages.findWhere({msgid});
if (message && !message.get('received')) {
message.save({'received': (new Date()).toISOString()});
}
return true;
}
}
return false;
},
/**
* Given a {@link _converse.Message} return the XML stanza that represents it.
* @private
* @method _converse.ChatBox#createMessageStanza
* @param { _converse.Message } message - The message object
*/
createMessageStanza (message) {
const stanza = $msg({
'from': _converse.connection.jid,
'to': this.get('jid'),
'type': this.get('message_type'),
'id': message.get('edited') && u.getUniqueId() || message.get('msgid'),
}).c('body').t(message.get('message')).up()
.c(_converse.ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).root();
if (message.get('type') === 'chat') {
stanza.c('request', {'xmlns': Strophe.NS.RECEIPTS}).root();
}
if (message.get('is_spoiler')) {
if (message.get('spoiler_hint')) {
stanza.c('spoiler', {'xmlns': Strophe.NS.SPOILER}, message.get('spoiler_hint')).root();
} else {
stanza.c('spoiler', {'xmlns': Strophe.NS.SPOILER}).root();
}
}
(message.get('references') || []).forEach(reference => {
const attrs = {
'xmlns': Strophe.NS.REFERENCE,
'begin': reference.begin,
'end': reference.end,
'type': reference.type,
}
if (reference.uri) {
attrs.uri = reference.uri;
}
stanza.c('reference', attrs).root();
});
if (message.get('oob_url')) {
stanza.c('x', {'xmlns': Strophe.NS.OUTOFBAND}).c('url').t(message.get('oob_url')).root();
}
if (message.get('edited')) {
stanza.c('replace', {
'xmlns': Strophe.NS.MESSAGE_CORRECT,
'id': message.get('msgid')
}).root();
}
if (message.get('origin_id')) {
stanza.c('origin-id', {'xmlns': Strophe.NS.SID, 'id': message.get('origin_id')}).root();
}
return stanza;
},
getOutgoingMessageAttributes (text, spoiler_hint) {
const is_spoiler = this.get('composing_spoiler');
const origin_id = u.getUniqueId();
return {
'id': origin_id,
'jid': this.get('jid'),
'nickname': this.get('nickname'),
'msgid': origin_id,
'origin_id': origin_id,
'fullname': _converse.xmppstatus.get('fullname'),
'from': _converse.bare_jid,
'is_single_emoji': text ? u.isSingleEmoji(text) : false,
'sender': 'me',
'time': (new Date()).toISOString(),
'message': text ? u.httpToGeoUri(u.shortnameToUnicode(text), _converse) : undefined,
'is_spoiler': is_spoiler,
'spoiler_hint': is_spoiler ? spoiler_hint : undefined,
'type': this.get('message_type')
}
},
/**
* Responsible for setting the editable attribute of messages.
* If _converse.allow_message_corrections is "last", then only the last
* message sent from me will be editable. If set to "all" all messages
* will be editable. Otherwise no messages will be editable.
* @method _converse.ChatBox#setEditable
* @memberOf _converse.ChatBox
* @param { Object } attrs An object containing message attributes.
* @param { String } send_time - time when the message was sent
*/
setEditable (attrs, send_time, stanza) {
if (stanza && u.isHeadlineMessage(_converse, stanza)) {
return;
}
if (u.isEmptyMessage(attrs) || attrs.sender !== 'me') {
return;
}
if (_converse.allow_message_corrections === 'all') {
attrs.editable = !(attrs.file || 'oob_url' in attrs);
} else if ((_converse.allow_message_corrections === 'last') &&
(send_time > this.get('time_sent'))) {
this.set({'time_sent': send_time});
const msg = this.messages.findWhere({'editable': true});
if (msg) {
msg.save({'editable': false});
}
attrs.editable = !(attrs.file || 'oob_url' in attrs);
}
},
/**
* Responsible for sending off a text message inside an ongoing chat conversation.
* @method _converse.ChatBox#sendMessage
* @memberOf _converse.ChatBox
* @param { String } text - The chat message text
* @param { String } spoiler_hint - An optional hint, if the message being sent is a spoiler
* @returns { _converse.Message }
* @example
* const chat = _converse.api.chats.get('buddy1@example.com');
* chat.sendMessage('hello world');
*/
sendMessage (text, spoiler_hint) {
const attrs = this.getOutgoingMessageAttributes(text, spoiler_hint);
let message = this.messages.findWhere('correcting')
if (message) {
const older_versions = message.get('older_versions') || {};
older_versions[message.get('time')] = message.get('message');
message.save({
'correcting': false,
'edited': (new Date()).toISOString(),
'message': attrs.message,
'older_versions': older_versions,
'references': attrs.references,
'is_single_emoji': attrs.message ? u.isSingleEmoji(attrs.message) : false,
'origin_id': u.getUniqueId(),
'received': undefined
});
} else {
this.setEditable(attrs, (new Date()).toISOString());
message = this.messages.create(attrs);
}
_converse.api.send(this.createMessageStanza(message));
return message;
},
/**
* Sends a message with the current XEP-0085 chat state of the user
* as taken from the `chat_state` attribute of the {@link _converse.ChatBox}.
* @private
* @method _converse.ChatBox#sendChatState
*/
sendChatState () {
if (_converse.send_chat_state_notifications && this.get('chat_state')) {
const allowed = _converse.send_chat_state_notifications;
if (Array.isArray(allowed) && !allowed.includes(this.get('chat_state'))) {
return;
}
_converse.api.send(
$msg({
'id': u.getUniqueId(),
'to': this.get('jid'),
'type': 'chat'
}).c(this.get('chat_state'), {'xmlns': Strophe.NS.CHATSTATES}).up()
.c('no-store', {'xmlns': Strophe.NS.HINTS}).up()
.c('no-permanent-store', {'xmlns': Strophe.NS.HINTS})
);
}
},
async sendFiles (files) {
const result = await _converse.api.disco.features.get(Strophe.NS.HTTPUPLOAD, _converse.domain);
const item = result.pop();
if (!item) {
this.messages.create({
'message': __("Sorry, looks like file upload is not supported by your server."),
'type': 'error',
'ephemeral': true
});
return;
}
const data = item.dataforms.where({'FORM_TYPE': {'value': Strophe.NS.HTTPUPLOAD, 'type': "hidden"}}).pop(),
max_file_size = window.parseInt(get(data, 'attributes.max-file-size.value')),
slot_request_url = get(item, 'id');
if (!slot_request_url) {
this.messages.create({
'message': __("Sorry, looks like file upload is not supported by your server."),
'type': 'error',
'ephemeral': true
});
return;
}
Array.from(files).forEach(file => {
if (!window.isNaN(max_file_size) && window.parseInt(file.size) > max_file_size) {
return this.messages.create({
'message': __('The size of your file, %1$s, exceeds the maximum allowed by your server, which is %2$s.',
file.name, filesize(max_file_size)),
'type': 'error',
'ephemeral': true
});
} else {
const attrs = Object.assign(
this.getOutgoingMessageAttributes(), {
'file': true,
'progress': 0,
'slot_request_url': slot_request_url
});
this.setEditable(attrs, (new Date()).toISOString());
const message = this.messages.create(attrs, {'silent': true});
message.file = file;
this.messages.trigger('add', message);
message.getRequestSlotURL();
}
});
},
getReferencesFromStanza (stanza) {
const text = propertyOf(stanza.querySelector('body'))('textContent');
return sizzle(`reference[xmlns="${Strophe.NS.REFERENCE}"]`, stanza).map(ref => {
const begin = ref.getAttribute('begin'),
end = ref.getAttribute('end');
return {
'begin': begin,
'end': end,
'type': ref.getAttribute('type'),
'value': text.slice(begin, end),
'uri': ref.getAttribute('uri')
};
});
},
/**
* Extract the XEP-0359 stanza IDs from the passed in stanza
* and return a map containing them.
* @private
* @method _converse.ChatBox#getStanzaIDs
* @param { XMLElement } stanza - The message stanza
*/
getStanzaIDs (stanza) {
const attrs = {};
const stanza_ids = sizzle(`stanza-id[xmlns="${Strophe.NS.SID}"]`, stanza);
if (stanza_ids.length) {
stanza_ids.forEach(s => (attrs[`stanza_id ${s.getAttribute('by')}`] = s.getAttribute('id')));
}
const result = sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, stanza).pop();
if (result) {
const by_jid = stanza.getAttribute('from');
attrs[`stanza_id ${by_jid}`] = result.getAttribute('id');
}
const origin_id = sizzle(`origin-id[xmlns="${Strophe.NS.SID}"]`, stanza).pop();
if (origin_id) {
attrs['origin_id'] = origin_id.getAttribute('id');
}
return attrs;
},
isArchived (original_stanza) {
return !!sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop();
},
getErrorMessage (stanza) {
const error = stanza.querySelector('error');
return propertyOf(error.querySelector('text'))('textContent') ||
__('Sorry, an error occurred:') + ' ' + error.innerHTML;
},
/**
* Given a message stanza, return the text contained in its body.
* @private
* @param { XMLElement } stanza
*/
getMessageBody (stanza) {
const type = stanza.getAttribute('type');
if (type === 'error') {
return this.getErrorMessage(stanza);
} else {
const body = stanza.querySelector('body');
if (body) {
return body.textContent.trim();
}
}
},
/**
* Parses a passed in message stanza and returns an object
* of attributes.
* @private
* @method _converse.ChatBox#getMessageAttributesFromStanza
* @param { XMLElement } stanza - The message stanza
* @param { XMLElement } delay - The <delay> node from the stanza, if there was one.
* @param { XMLElement } original_stanza - The original stanza, that contains the
* message stanza, if it was contained, otherwise it's the message stanza itself.
*/
async getMessageAttributesFromStanza (stanza, original_stanza) {
const spoiler = sizzle(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`, original_stanza).pop();
const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop();
const text = this.getMessageBody(stanza) || undefined;
const chat_state = stanza.getElementsByTagName(_converse.COMPOSING).length && _converse.COMPOSING ||
stanza.getElementsByTagName(_converse.PAUSED).length && _converse.PAUSED ||
stanza.getElementsByTagName(_converse.INACTIVE).length && _converse.INACTIVE ||
stanza.getElementsByTagName(_converse.ACTIVE).length && _converse.ACTIVE ||
stanza.getElementsByTagName(_converse.GONE).length && _converse.GONE;
const replaced_id = this.getReplaceId(stanza)
const msgid = replaced_id || stanza.getAttribute('id') || original_stanza.getAttribute('id');
const attrs = Object.assign({
'chat_state': chat_state,
'is_archived': this.isArchived(original_stanza),
'is_delayed': !!delay,
'is_single_emoji': text ? await u.isSingleEmoji(text) : false,
'is_spoiler': !!spoiler,
'message': text,
'msgid': msgid,
'replaced_id': replaced_id,
'references': this.getReferencesFromStanza(stanza),
'subject': propertyOf(stanza.querySelector('subject'))('textContent'),
'thread': propertyOf(stanza.querySelector('thread'))('textContent'),
'time': delay ? dayjs(delay.getAttribute('stamp')).toISOString() : (new Date()).toISOString(),
'type': stanza.getAttribute('type')
}, this.getStanzaIDs(original_stanza));
if (attrs.type === 'groupchat') {
attrs.from = stanza.getAttribute('from');
attrs.nick = Strophe.unescapeNode(Strophe.getResourceFromJid(attrs.from));
attrs.sender = attrs.nick === this.get('nick') ? 'me': 'them';
attrs.received = (new Date()).toISOString();
} else {
attrs.from = Strophe.getBareJidFromJid(stanza.getAttribute('from'));
if (attrs.from === _converse.bare_jid) {
attrs.sender = 'me';
attrs.fullname = _converse.xmppstatus.get('fullname');
} else {
attrs.sender = 'them';
attrs.fullname = this.get('fullname');
}
}
sizzle(`x[xmlns="${Strophe.NS.OUTOFBAND}"]`, stanza).forEach(xform => {
attrs['oob_url'] = xform.querySelector('url').textContent;
attrs['oob_desc'] = xform.querySelector('url').textContent;
});
if (spoiler) {
attrs.spoiler_hint = spoiler.textContent.length > 0 ? spoiler.textContent : '';
}
if (replaced_id) {
attrs['edited'] = (new Date()).toISOString();
}
// We prefer to use one of the XEP-0359 unique and stable stanza IDs as the Model id, to avoid duplicates.
attrs['id'] = attrs['origin_id'] ||
attrs[`stanza_id ${attrs.from}`] ||
u.getUniqueId();
return attrs;
},
maybeShow () {
// Returns the chatbox
return this.trigger("show");
},
/**
* Indicates whether the chat is hidden and therefore
* whether a newly received message will be visible
* to the user or not.
* @returns {boolean}
*/
isHidden () {
return this.get('hidden') ||
this.get('minimized') ||
this.isScrolledUp() ||
_converse.windowState === 'hidden';
},
/**
* Given a newly received {@link _converse.Message} instance,
* update the unread counter if necessary.
* @private
* @param {_converse.Message} message
*/
incrementUnreadMsgCounter (message) {
if (!message || !message.get('message')) {
return;
}
if (utils.isNewMessage(message) && this.isHidden()) {
this.save({'num_unread': this.get('num_unread') + 1});
_converse.incrementMsgCounter();
}
},
clearUnreadMsgCounter () {
u.safeSave(this, {'num_unread': 0});
},
isScrolledUp () {
return this.get('scrolled', true);
}
});
_converse.ChatBoxes = _converse.Collection.extend({
comparator: 'time_opened',
......@@ -1109,38 +68,6 @@ converse.plugins.add('converse-chatboxes', {
return new _converse.ChatBox(attrs, options);
},
registerMessageHandler () {
_converse.connection.addHandler(stanza => {
if (sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, stanza).pop()) {
// MAM messages are handled in converse-mam.
// We shouldn't get MAM messages here because
// they shouldn't have a `type` attribute.
_converse.log(`Received a MAM message with type "chat".`, Strophe.LogLevel.WARN);
return true;
}
this.onMessage(stanza);
return true;
}, null, 'message', 'chat');
_converse.connection.addHandler(stanza => {
// Message receipts are usually without the `type` attribute. See #1353
if (stanza.getAttribute('type') !== null) {
// TODO: currently Strophe has no way to register a handler
// for stanzas without a `type` attribute.
// We could update it to accept null to mean no attribute,
// but that would be a backward-incompatible change
return true; // Gets handled above.
}
this.onMessage(stanza);
return true;
}, Strophe.NS.RECEIPTS, 'message');
_converse.connection.addHandler(stanza => {
this.onErrorMessage(stanza);
return true;
}, null, 'message', 'error');
},
onChatBoxesFetched (collection) {
/* Show chat boxes upon receiving them from storage */
collection.filter(c => !c.isValid()).forEach(c => c.destroy());
......@@ -1162,238 +89,36 @@ converse.plugins.add('converse-chatboxes', {
return;
}
this.browserStorage = _converse.createStore(`converse.chatboxes-${_converse.bare_jid}`);
this.registerMessageHandler();
this.fetch({
'add': true,
'success': c => this.onChatBoxesFetched(c)
});
},
/**
* Handler method for all incoming error stanza stanzas.
* @private
* @method _converse.ChatBox#onErrorMessage
* @param { XMLElement } stanza - The error message stanza
*/
async onErrorMessage (stanza) {
const from_jid = Strophe.getBareJidFromJid(stanza.getAttribute('from'));
if (utils.isSameBareJID(from_jid, _converse.bare_jid)) {
return;
}
const chatbox = await this.getChatBox(from_jid);
if (!chatbox) {
return;
}
const should_show = await chatbox.shouldShowErrorMessage(stanza);
if (!should_show) {
return;
}
const attrs = await chatbox.getMessageAttributesFromStanza(stanza, stanza);
await chatbox.messages.create(attrs);
},
/**
* Reject an incoming message by replying with an error message of type "cancel".
* @private
* @method _converse.ChatBox#rejectMessage
* @param { XMLElement } stanza - The incoming message stanza
* @param { XMLElement } text - Text explaining why the message was rejected
*/
rejectMessage (stanza, text) {
_converse.api.send(
$msg({
'to': stanza.getAttribute('from'),
'type': 'error',
'id': stanza.getAttribute('id')
}).c('error', {'type': 'cancel'})
.c('not-allowed', {xmlns:"urn:ietf:params:xml:ns:xmpp-stanzas"}).up()
.c('text', {xmlns:"urn:ietf:params:xml:ns:xmpp-stanzas"}).t(text)
);
_converse.log(`Rejecting message stanza with the following reason: ${text}`, Strophe.LogLevel.WARN);
_converse.log(stanza, Strophe.LogLevel.WARN);
},
/**
* Handler method for all incoming single-user chat "message" stanzas.
* @private
* @method _converse.ChatBoxes#onMessage
* @param { XMLElement } stanza - The incoming message stanza
*/
async onMessage (stanza) {
const original_stanza = stanza;
let to_jid = stanza.getAttribute('to');
const to_resource = Strophe.getResourceFromJid(to_jid);
if (_converse.filter_by_resource && (to_resource && to_resource !== _converse.resource)) {
return _converse.log(
`onMessage: Ignoring incoming message intended for a different resource: ${to_jid}`,
Strophe.LogLevel.INFO
);
} else if (utils.isHeadlineMessage(_converse, stanza)) {
// XXX: Prosody sends headline messages with the
// wrong type ('chat'), so we need to filter them out here.
return _converse.log(
`onMessage: Ignoring incoming headline message from JID: ${stanza.getAttribute('from')}`,
Strophe.LogLevel.INFO
);
}
const bare_forward = sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).length;
if (bare_forward) {
return this.rejectMessage(
stanza,
'Forwarded messages not part of an encapsulating protocol are not supported'
);
}
let from_jid = stanza.getAttribute('from') || _converse.bare_jid;
const is_carbon = u.isCarbonMessage(stanza);
if (is_carbon) {
if (from_jid === _converse.bare_jid) {
const selector = `[xmlns="${Strophe.NS.CARBONS}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`;
stanza = sizzle(selector, stanza).pop();
to_jid = stanza.getAttribute('to');
from_jid = stanza.getAttribute('from');
} else {
// Prevent message forging via carbons: https://xmpp.org/extensions/xep-0280.html#security
return this.rejectMessage(stanza, 'Rejecting carbon from invalid JID');
}
}
const is_mam = u.isMAMMessage(stanza);
if (is_mam) {
if (from_jid === _converse.bare_jid) {
const selector = `[xmlns="${Strophe.NS.MAM}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`;
stanza = sizzle(selector, stanza).pop();
to_jid = stanza.getAttribute('to');
from_jid = stanza.getAttribute('from');
} else {
return _converse.log(
`onMessage: Ignoring alleged MAM message from ${stanza.getAttribute('from')}`,
Strophe.LogLevel.WARN
);
}
}
const from_bare_jid = Strophe.getBareJidFromJid(from_jid);
const is_me = from_bare_jid === _converse.bare_jid;
if (is_me && to_jid === null) {
return _converse.log(
`Don't know how to handle message stanza without 'to' attribute. ${stanza.outerHTML}`,
Strophe.LogLevel.ERROR
);
}
const contact_jid = is_me ? Strophe.getBareJidFromJid(to_jid) : from_bare_jid;
const contact = await _converse.api.contacts.get(contact_jid);
const is_roster_contact = contact !== undefined;
if (!is_me && !is_roster_contact && !_converse.allow_non_roster_messaging) {
return;
}
});
// Get chat box, but only create when the message has something to show to the user
const has_body = sizzle(`body, encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).length > 0;
const roster_nick = get(contact, 'attributes.nickname');
const chatbox = await this.getChatBox(contact_jid, {'nickname': roster_nick}, has_body);
if (chatbox) {
const message = await chatbox.getDuplicateMessage(stanza);
if (message) {
chatbox.updateMessage(message, original_stanza);
}
if (!message &&
!chatbox.handleReceipt (stanza, from_jid, is_carbon, is_me) &&
!chatbox.handleChatMarker(stanza, from_jid, is_carbon, is_roster_contact, is_mam)) {
const attrs = await chatbox.getMessageAttributesFromStanza(stanza, original_stanza);
chatbox.setEditable(attrs, attrs.time, stanza);
if (attrs['chat_state'] || !u.isEmptyMessage(attrs)) {
const msg = chatbox.correctMessage(attrs) || chatbox.messages.create(attrs);
chatbox.incrementUnreadMsgCounter(msg);
}
}
}
/**
* Triggered when a message stanza is been received and processed
* @event _converse#message
* @type { object }
* @property { _converse.ChatBox | _converse.ChatRoom } chatbox
* @property { XMLElement } stanza
* @example _converse.api.listen.on('message', obj => { ... });
*/
_converse.api.trigger('message', {'stanza': original_stanza, 'chatbox': chatbox});
},
/**
* Returns a chat box or optionally return a newly
* created one if one doesn't exist.
* @private
* @method _converse.ChatBox#getChatBox
* @param { string } jid - The JID of the user whose chat box we want
* @param { boolean } create - Should a new chat box be created if none exists?
* @param { object } attrs - Optional chat box atributes. If the
* chat box already exists, its attributes will be updated.
*/
async getChatBox (jid, attrs={}, create) {
if (isObject(jid)) {
create = attrs;
attrs = jid;
jid = attrs.jid;
}
async function createChatBox (jid, attrs, model) {
jid = Strophe.getBareJidFromJid(jid.toLowerCase());
await _converse.api.waitUntil('chatBoxesFetched');
let chatbox = this.get(Strophe.getBareJidFromJid(jid));
if (chatbox) {
chatbox.save(attrs);
} else if (create) {
Object.assign(attrs, {'jid': jid, 'id': jid});
chatbox = this.create(attrs, {
'error' (model, response) {
_converse.log(response.responseText);
let chatbox;
try {
chatbox = new model(attrs, {'collection': _converse.chatboxes});
} catch (e) {
_converse.log(e, Strophe.LogLevel.ERROR);
return null;
}
});
await chatbox.messages.fetched;
await chatbox.initialized;
if (!chatbox.isValid()) {
chatbox.destroy();
return null;
}
}
_converse.chatboxes.add(chatbox);
await chatbox.messages.fetched;
return chatbox;
}
});
function autoJoinChats () {
/* Automatically join private chats, based on the
* "auto_join_private_chats" configuration setting.
*/
_converse.auto_join_private_chats.forEach(jid => {
if (_converse.chatboxes.where({'jid': jid}).length) {
return;
}
if (isString(jid)) {
_converse.api.chats.open(jid);
} else {
_converse.log(
'Invalid jid criteria specified for "auto_join_private_chats"',
Strophe.LogLevel.ERROR);
}
});
/**
* Triggered once any private chats have been automatically joined as
* specified by the `auto_join_private_chats` setting.
* See: https://conversejs.org/docs/html/configuration.html#auto-join-private-chats
* @event _converse#privateChatsAutoJoined
* @example _converse.api.listen.on('privateChatsAutoJoined', () => { ... });
* @example _converse.api.waitUntil('privateChatsAutoJoined').then(() => { ... });
*/
_converse.api.trigger('privateChatsAutoJoined');
}
/************************ BEGIN Event Handlers ************************/
_converse.api.listen.on('chatBoxesFetched', autoJoinChats);
_converse.api.listen.on('addClientFeatures', () => {
_converse.api.disco.own.features.add(Strophe.NS.MESSAGE_CORRECT);
_converse.api.disco.own.features.add(Strophe.NS.HTTPUPLOAD);
......@@ -1411,13 +136,6 @@ converse.plugins.add('converse-chatboxes', {
_converse.api.trigger('chatBoxesInitialized');
});
_converse.api.listen.on('clearSession', () => {
if (_converse.shouldClearCache()) {
_converse.chatboxes.filter(c => c.messages && c.messages.clearSession({'silent': true}));
}
});
_converse.api.listen.on('presencesInitialized', (reconnecting) => _converse.chatboxes.onConnected(reconnecting));
_converse.api.listen.on('reconnected', () => _converse.chatboxes.forEach(m => m.onReconnection()));
_converse.api.listen.on('windowStateChanged', d => (d.state === 'visible') && _converse.clearMsgCounter());
......@@ -1427,133 +145,41 @@ converse.plugins.add('converse-chatboxes', {
/************************ BEGIN API ************************/
Object.assign(_converse.api, {
/**
* The "chats" namespace (used for one-on-one chats)
* The "chatboxes" namespace.
*
* @namespace _converse.api.chats
* @namespace _converse.api.chatboxes
* @memberOf _converse.api
*/
chats: {
chatboxes: {
/**
* @method _converse.api.chats.create
* @param {string|string[]} jid|jids An jid or array of jids
* @param {object} [attrs] An object containing configuration attributes.
* @param { String|String[] } jids - A JID or array of JIDs
* @param { Object } [attrs] An object containing configuration attributes
* @param { Backbone.Model } model - The type of chatbox that should be created
*/
async create (jids, attrs) {
if (isString(jids)) {
if (attrs && !get(attrs, 'fullname')) {
const contact = await _converse.api.contacts.get(jids);
attrs.fullname = get(contact, 'attributes.fullname');
}
const chatbox = _converse.chatboxes.getChatBox(jids, attrs, true);
if (!chatbox) {
_converse.log("Could not open chatbox for JID: "+jids, Strophe.LogLevel.ERROR);
return;
}
return chatbox;
}
if (Array.isArray(jids)) {
return Promise.all(jids.forEach(async jid => {
const contact = await _converse.api.contacts.get(jids);
attrs.fullname = get(contact, 'attributes.fullname');
return _converse.chatboxes.getChatBox(jid, attrs, true).maybeShow();
}));
}
_converse.log(
"chats.create: You need to provide at least one JID",
Strophe.LogLevel.ERROR
);
return null;
},
/**
* Opens a new one-on-one chat.
*
* @method _converse.api.chats.open
* @param {String|string[]} name - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
* @param {Object} [attrs] - Attributes to be set on the _converse.ChatBox model.
* @param {Boolean} [attrs.minimized] - Should the chat be
* created in minimized state.
* @param {Boolean} [force=false] - By default, a minimized
* chat won't be maximized (in `overlayed` view mode) and in
* `fullscreen` view mode a newly opened chat won't replace
* another chat already in the foreground.
* Set `force` to `true` if you want to force the chat to be
* maximized or shown.
* @returns {Promise} Promise which resolves with the
* _converse.ChatBox representing the chat.
*
* @example
* // To open a single chat, provide the JID of the contact you're chatting with in that chat:
* converse.plugins.add('myplugin', {
* initialize: function() {
* const _converse = this._converse;
* // Note, buddy@example.org must be in your contacts roster!
* _converse.api.chats.open('buddy@example.com').then(chat => {
* // Now you can do something with the chat model
* });
* }
* });
*
* @example
* // To open an array of chats, provide an array of JIDs:
* converse.plugins.add('myplugin', {
* initialize: function () {
* const _converse = this._converse;
* // Note, these users must first be in your contacts roster!
* _converse.api.chats.open(['buddy1@example.com', 'buddy2@example.com']).then(chats => {
* // Now you can do something with the chat models
* });
* }
* });
*/
async open (jids, attrs, force) {
async create (jids=[], attrs={}, model) {
await _converse.api.waitUntil('chatBoxesFetched');
if (isString(jids)) {
const chat = await _converse.api.chats.create(jids, attrs);
if (chat) {
return chat.maybeShow(force);
}
return chat;
} else if (Array.isArray(jids)) {
return Promise.all(
jids.map(j => _converse.api.chats.create(j, attrs).then(c => c && c.maybeShow(force)))
.filter(c => c)
);
return createChatBox(jids, attrs, model);
} else {
return Promise.all(jids.map(jid => createChatBox(jid, attrs, model)));
}
const err_msg = "chats.open: You need to provide at least one JID";
_converse.log(err_msg, Strophe.LogLevel.ERROR);
throw new Error(err_msg);
},
/**
* Retrieves a chat model. The chat should already be open.
*
* @method _converse.api.chats.get
* @param {String|string[]} jids - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
* @returns { Promise<_converse.ChatBox> }
*
* @example
* // To return a single chat, provide the JID of the contact you're chatting with in that chat:
* const model = _converse.api.chats.get('buddy@example.com');
*
* @example
* // To return an array of chats, provide an array of JIDs:
* const models = _converse.api.chats.get(['buddy1@example.com', 'buddy2@example.com']);
*
* @example
* // To return all open chats, call the method without any parameters::
* const models = _converse.api.chats.get();
*
* @param { String|String[] } jids - A JID or array of JIDs
*/
get (jids) {
async get (jids) {
await _converse.api.waitUntil('chatBoxesFetched');
if (jids === undefined) {
// FIXME: Leaky abstraction from MUC. We need to add a
// base type for chat boxes, and check for that.
return _converse.chatboxes.filter(c => (c.get('type') !== _converse.CHATROOMS_TYPE));
return _converse.chatboxes.models;
} else if (isString(jids)) {
return _converse.chatboxes.getChatBox(jids);
return _converse.chatboxes.get(jids.toLowerCase());
} else {
jids = jids.map(j => j.toLowerCase());
return _converse.chatboxes.models.filter(m => jids.includes(m.get('jid')));
}
return Promise.all(jids.map(jid => _converse.chatboxes.getChatBox(jid, {}, true)));
}
}
});
......
......@@ -78,10 +78,12 @@ const CORE_PLUGINS = [
'converse-bosh',
'converse-caps',
'converse-chatboxes',
'converse-chat',
'converse-disco',
'converse-emoji',
'converse-mam',
'converse-muc',
'converse-headlines',
'converse-ping',
'converse-pubsub',
'converse-roster',
......
......@@ -8,12 +8,12 @@
*/
import "converse-chatview";
import converse from "@converse/headless/converse-core";
import tpl_chatbox from "templates/chatbox.html";
import { isString } from "lodash";
const { utils } = converse.env;
converse.plugins.add('converse-headline', {
converse.plugins.add('converse-headlines', {
/* Plugin dependencies are other plugins which might be
* overridden or relied upon, and therefore need to be loaded before
* this plugin.
......@@ -24,7 +24,7 @@ converse.plugins.add('converse-headline', {
*
* NB: These plugins need to have already been loaded via require.js.
*/
dependencies: ["converse-chatview"],
dependencies: ["converse-chat"],
overrides: {
// Overrides mentioned here will be picked up by converse.js's
......@@ -71,52 +71,8 @@ converse.plugins.add('converse-headline', {
});
_converse.HeadlinesBoxView = _converse.ChatBoxView.extend({
className: 'chatbox headlines',
events: {
'click .close-chatbox-button': 'close',
'click .toggle-chatbox-button': 'minimize',
'keypress textarea.chat-textarea': 'onKeyDown'
},
initialize () {
this.initDebounced();
this.model.disable_mam = true; // Don't do MAM queries for this box
this.listenTo(this.model.messages, 'add', this.onMessageAdded);
this.listenTo(this.model, 'show', this.show);
this.listenTo(this.model, 'destroy', this.hide);
this.listenTo(this.model, 'change:minimized', this.onMinimizedChanged);
this.render().insertHeading()
this.updateAfterMessagesFetched();
this.insertIntoDOM().hide();
_converse.api.trigger('chatBoxInitialized', this);
},
render () {
this.el.setAttribute('id', this.model.get('box_id'))
this.el.innerHTML = tpl_chatbox(
Object.assign(this.model.toJSON(), {
info_close: '',
label_personal_message: '',
show_send_button: false,
show_toolbar: false,
unread_msgs: ''
}
));
this.content = this.el.querySelector('.chat-content');
return this;
},
// Override to avoid the methods in converse-chatview.js
'renderMessageForm': function renderMessageForm () {},
'afterShown': function afterShown () {}
});
async function onHeadlineMessage (message) {
/* Handler method for all incoming messages of type "headline". */
// Handler method for all incoming messages of type "headline".
if (utils.isHeadlineMessage(_converse, message)) {
const from_jid = message.getAttribute('from');
if (from_jid.includes('@') &&
......@@ -140,6 +96,8 @@ converse.plugins.add('converse-headline', {
}
}
/************************ BEGIN Event Handlers ************************/
function registerHeadlineHandler () {
_converse.connection.addHandler(message => {
onHeadlineMessage(message);
......@@ -148,15 +106,52 @@ converse.plugins.add('converse-headline', {
}
_converse.api.listen.on('connected', registerHeadlineHandler);
_converse.api.listen.on('reconnected', registerHeadlineHandler);
/************************ END Event Handlers ************************/
_converse.api.listen.on('chatBoxViewsInitialized', () => {
const views = _converse.chatboxviews;
_converse.chatboxes.on('add', item => {
if (!views.get(item.get('id')) && item.get('type') === _converse.HEADLINES_TYPE) {
views.add(item.get('id'), new _converse.HeadlinesBoxView({model: item}));
/************************ BEGIN API ************************/
Object.assign(_converse.api, {
/**
* The "headlines" namespace, which is used for headline-channels
* which are read-only channels containing messages of type
* "headline".
*
* @namespace _converse.api.headlines
* @memberOf _converse.api
*/
headlines: {
/**
* Retrieves a headline-channel or all headline-channels.
*
* @method _converse.api.headlines.get
* @param {String|String[]} jids - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
* @param {Object} [attrs] - Attributes to be set on the _converse.ChatBox model.
* @param {Boolean} [create=false] - Whether the chat should be created if it's not found.
* @returns { Promise<_converse.HeadlinesBox> }
*/
async get (jids, attrs={}, create=false) {
async function _get (jid) {
let model = await _converse.api.chatboxes.get(jid);
if (!model && create) {
model = await _converse.api.chatboxes.create(jid, attrs, _converse.HeadlinesBox);
} else {
model = (model && model.get('type') === _converse.HEADLINES_TYPE) ? model : null;
if (model && Object.keys(attrs).length) {
model.save(attrs);
}
}
return model;
}
if (jids === undefined) {
const chats = await _converse.api.chatboxes.get();
return chats.filter(c => (c.get('type') === _converse.HEADLINES_TYPE));
} else if (isString(jids)) {
return _get(jids);
}
return Promise.all(jids.map(jid => _get(jid)));
}
}
});
});
/************************ END API ************************/
}
});
......@@ -117,7 +117,7 @@ converse.plugins.add('converse-mam', {
}
const message_handler = is_groupchat ?
this.onMessage.bind(this) :
_converse.chatboxes.onMessage.bind(_converse.chatboxes);
_converse.handleMessageStanza.bind(_converse.chatboxes);
const query = Object.assign({
'groupchat': is_groupchat,
......
......@@ -12,6 +12,7 @@
import "./converse-disco";
import "./converse-emoji";
import "./utils/muc";
import { clone, get, intersection, invoke, isElement, isObject, isString, uniq, zipObject } from "lodash";
import converse from "./converse-core";
import u from "./utils/form";
......@@ -22,7 +23,7 @@ const MUC_ROLE_WEIGHTS = {
'none': 2,
};
const { Strophe, Backbone, $iq, $build, $msg, $pres, sizzle, _ } = converse.env;
const { Strophe, Backbone, $iq, $build, $msg, $pres, sizzle } = converse.env;
// Add Strophe Namespaces
Strophe.addNamespace('MUC_ADMIN', Strophe.NS.MUC + "#admin");
......@@ -81,13 +82,13 @@ converse.plugins.add('converse-muc', {
*
* NB: These plugins need to have already been loaded via require.js.
*/
dependencies: ["converse-chatboxes", "converse-disco", "converse-controlbox"],
dependencies: ["converse-chatboxes", "converse-chat", "converse-disco", "converse-controlbox"],
overrides: {
tearDown () {
const { _converse } = this.__super__;
const groupchats = this.chatboxes.where({'type': _converse.CHATROOMS_TYPE});
_.each(groupchats, gc => u.safeSave(gc, {'connection_status': converse.ROOMSTATUS.DISCONNECTED}));
groupchats.forEach(gc => u.safeSave(gc, {'connection_status': converse.ROOMSTATUS.DISCONNECTED}));
this.__super__.tearDown.call(this, arguments);
},
......@@ -129,7 +130,7 @@ converse.plugins.add('converse-muc', {
});
_converse.api.promises.add(['roomsAutoJoined']);
if (_converse.locked_muc_domain && !_.isString(_converse.muc_domain)) {
if (_converse.locked_muc_domain && !isString(_converse.muc_domain)) {
throw new Error("Config Error: it makes no sense to set locked_muc_domain "+
"to true when muc_domain is not set");
}
......@@ -234,7 +235,7 @@ converse.plugins.add('converse-muc', {
*/
settings.type = _converse.CHATROOMS_TYPE;
settings.id = jid;
const chatbox = await _converse.chatboxes.getChatBox(jid, settings, true);
const chatbox = await _converse.api.rooms.get(jid, settings, true);
chatbox.maybeShow(true);
return chatbox;
}
......@@ -263,7 +264,7 @@ converse.plugins.add('converse-muc', {
onOccupantRemoved () {
this.stopListening(this.occupant);
delete this.occupant;
const chatbox = _.get(this, 'collection.chatbox');
const chatbox = get(this, 'collection.chatbox');
if (!chatbox) {
return _converse.log(
`Could not get collection.chatbox for message: ${JSON.stringify(this.toJSON())}`,
......@@ -277,7 +278,7 @@ converse.plugins.add('converse-muc', {
if (occupant.get('nick') === Strophe.getResourceFromJid(this.get('from'))) {
this.occupant = occupant;
this.listenTo(this.occupant, 'destroy', this.onOccupantRemoved);
const chatbox = _.get(this, 'collection.chatbox');
const chatbox = get(this, 'collection.chatbox');
if (!chatbox) {
return _converse.log(
`Could not get collection.chatbox for message: ${JSON.stringify(this.toJSON())}`,
......@@ -290,7 +291,7 @@ converse.plugins.add('converse-muc', {
setOccupant () {
if (this.get('type') !== 'groupchat') { return; }
const chatbox = _.get(this, 'collection.chatbox');
const chatbox = get(this, 'collection.chatbox');
if (!chatbox) {
return _converse.log(
`Could not get collection.chatbox for message: ${JSON.stringify(this.toJSON())}`,
......@@ -308,7 +309,7 @@ converse.plugins.add('converse-muc', {
},
getVCardForChatroomOccupant () {
const chatbox = _.get(this, 'collection.chatbox');
const chatbox = get(this, 'collection.chatbox');
const nick = Strophe.getResourceFromJid(this.get('from'));
if (chatbox && chatbox.get('nick') === nick) {
......@@ -488,7 +489,7 @@ converse.plugins.add('converse-muc', {
initFeatures () {
const id = `converse.muc-features-${_converse.bare_jid}-${this.get('jid')}`;
this.features = new Backbone.Model(
_.assign({id}, _.zipObject(converse.ROOM_FEATURES, converse.ROOM_FEATURES.map(_.stubFalse)))
Object.assign({id}, zipObject(converse.ROOM_FEATURES, converse.ROOM_FEATURES.map(() => false)))
);
this.features.browserStorage = _converse.createStore(id, "session");
},
......@@ -855,13 +856,13 @@ converse.plugins.add('converse-muc', {
const fields = await _converse.api.disco.getFields(this.get('jid'));
this.save({
'name': identity && identity.get('name'),
'description': _.get(fields.findWhere({'var': "muc#roominfo_description"}), 'attributes.value')
'description': get(fields.findWhere({'var': "muc#roominfo_description"}), 'attributes.value')
}
);
const features = await _converse.api.disco.getFeatures(this.get('jid'));
const attrs = Object.assign(
_.zipObject(converse.ROOM_FEATURES, converse.ROOM_FEATURES.map(_.stubFalse)),
zipObject(converse.ROOM_FEATURES, converse.ROOM_FEATURES.map(() => false)),
{'fetched': (new Date()).toISOString()}
);
features.each(feature => {
......@@ -874,7 +875,7 @@ converse.plugins.add('converse-muc', {
}
attrs[fieldname.replace('muc_', '')] = true;
});
attrs.description = _.get(fields.findWhere({'var': "muc#roominfo_description"}), 'attributes.value');
attrs.description = get(fields.findWhere({'var': "muc#roominfo_description"}), 'attributes.value');
this.features.save(attrs);
},
......@@ -1006,7 +1007,7 @@ converse.plugins.add('converse-muc', {
* @returns { ('none'|'visitor'|'participant'|'moderator') }
*/
getOwnRole () {
return _.get(this.getOwnOccupant(), 'attributes.role');
return get(this.getOwnOccupant(), 'attributes.role');
},
/**
......@@ -1016,7 +1017,7 @@ converse.plugins.add('converse-muc', {
* @returns { ('none'|'outcast'|'member'|'admin'|'owner') }
*/
getOwnAffiliation () {
return _.get(this.getOwnOccupant(), 'attributes.affiliation');
return get(this.getOwnOccupant(), 'attributes.affiliation');
},
/**
......@@ -1066,7 +1067,7 @@ converse.plugins.add('converse-muc', {
* @returns { Promise }
*/
setAffiliations (members) {
const affiliations = _.uniq(members.map(m => m.affiliation));
const affiliations = uniq(members.map(m => m.affiliation));
return Promise.all(affiliations.map(a => this.setAffiliation(a, members)));
},
......@@ -1280,8 +1281,8 @@ converse.plugins.add('converse-muc', {
}
const jid = data.jid || '';
const attributes = Object.assign(data, {
'jid': Strophe.getBareJidFromJid(jid) || _.get(occupant, 'attributes.jid'),
'resource': Strophe.getResourceFromJid(jid) || _.get(occupant, 'attributes.resource')
'jid': Strophe.getBareJidFromJid(jid) || get(occupant, 'attributes.jid'),
'resource': Strophe.getResourceFromJid(jid) || get(occupant, 'attributes.resource')
});
if (occupant) {
occupant.save(attributes);
......@@ -1326,7 +1327,7 @@ converse.plugins.add('converse-muc', {
}
});
} else if (child.getAttribute("xmlns") === Strophe.NS.VCARDUPDATE) {
data.image_hash = _.get(child.querySelector('photo'), 'textContent');
data.image_hash = get(child.querySelector('photo'), 'textContent');
}
}
});
......@@ -1415,7 +1416,7 @@ converse.plugins.add('converse-muc', {
*/
isOwnMessage (msg) {
let from;
if (_.isElement(msg)) {
if (isElement(msg)) {
from = msg.getAttribute('from');
} else if (msg instanceof _converse.Message) {
from = msg.get('from');
......@@ -1460,7 +1461,7 @@ converse.plugins.add('converse-muc', {
await _converse.api.sendIQ(ping);
} catch (e) {
const sel = `error not-acceptable[xmlns="${Strophe.NS.STANZAS}"]`;
if (_.isElement(e) && sizzle(sel, e).length) {
if (isElement(e) && sizzle(sel, e).length) {
return false;
}
}
......@@ -1576,7 +1577,7 @@ converse.plugins.add('converse-muc', {
handleModifyError(pres) {
const text = _.get(pres.querySelector('error text'), 'textContent');
const text = get(pres.querySelector('error text'), 'textContent');
if (text) {
if (this.get('connection_status') === converse.ROOMSTATUS.CONNECTING) {
this.setDisconnectionMessage(text);
......@@ -1598,7 +1599,7 @@ converse.plugins.add('converse-muc', {
return;
}
const codes = sizzle('status', x).map(s => s.getAttribute('code'));
const disconnection_codes = _.intersection(codes, Object.keys(_converse.muc.disconnect_messages));
const disconnection_codes = intersection(codes, Object.keys(_converse.muc.disconnect_messages));
const disconnected = is_self && disconnection_codes.length > 0;
if (!disconnected) {
return;
......@@ -1608,8 +1609,8 @@ converse.plugins.add('converse-muc', {
// element. This appears to be a safe assumption, since
// each <x/> element pertains to a single user.
const item = x.querySelector('item');
const reason = item ? _.get(item.querySelector('reason'), 'textContent') : undefined;
const actor = item ? _.invoke(item.querySelector('actor'), 'getAttribute', 'nick') : undefined;
const reason = item ? get(item.querySelector('reason'), 'textContent') : undefined;
const actor = item ? invoke(item.querySelector('actor'), 'getAttribute', 'nick') : undefined;
const message = _converse.muc.disconnect_messages[disconnection_codes[0]];
this.setDisconnectionMessage(message, reason, actor);
},
......@@ -1639,8 +1640,8 @@ converse.plugins.add('converse-muc', {
const nick = Strophe.getResourceFromJid(stanza.getAttribute('from'));
message = __(_converse.muc.action_info_messages[code], nick);
const item = x.querySelector('item');
const reason = item ? _.get(item.querySelector('reason'), 'textContent') : undefined;
const actor = item ? _.invoke(item.querySelector('actor'), 'getAttribute', 'nick') : undefined;
const reason = item ? get(item.querySelector('reason'), 'textContent') : undefined;
const actor = item ? invoke(item.querySelector('actor'), 'getAttribute', 'nick') : undefined;
if (actor) {
message += '\n' + __('This action was done by %1$s.', actor);
}
......@@ -1705,7 +1706,7 @@ converse.plugins.add('converse-muc', {
onErrorPresence (stanza) {
const error = stanza.querySelector('error');
const error_type = error.getAttribute('type');
const reason = _.get(sizzle(`text[xmlns="${Strophe.NS.STANZAS}"]`, error).pop(), 'textContent');
const reason = get(sizzle(`text[xmlns="${Strophe.NS.STANZAS}"]`, error).pop(), 'textContent');
if (error_type === 'modify') {
this.handleModifyError(stanza);
......@@ -1731,7 +1732,7 @@ converse.plugins.add('converse-muc', {
const message = __("Your nickname doesn't conform to this groupchat's policies.");
this.setDisconnectionMessage(message, reason);
} else if (sizzle(`gone[xmlns="${Strophe.NS.STANZAS}"]`, error).length) {
const moved_jid = _.get(sizzle(`gone[xmlns="${Strophe.NS.STANZAS}"]`, error).pop(), 'textContent')
const moved_jid = get(sizzle(`gone[xmlns="${Strophe.NS.STANZAS}"]`, error).pop(), 'textContent')
.replace(/^xmpp:/, '')
.replace(/\?join$/, '');
this.save({
......@@ -2073,18 +2074,11 @@ converse.plugins.add('converse-muc', {
_converse.api.listen.on('reconnected', registerDirectInvitationHandler);
}
const getChatRoom = function (jid, attrs, create) {
jid = jid.toLowerCase();
attrs.type = _converse.CHATROOMS_TYPE;
attrs.id = jid;
return _converse.chatboxes.getChatBox(jid, attrs, create);
};
const createChatRoom = function (jid, attrs) {
if (jid.startsWith('xmpp:') && jid.endsWith('?join')) {
jid = jid.replace(/^xmpp:/, '').replace(/\?join$/, '');
}
return getChatRoom(jid, attrs, true);
return _converse.api.rooms.get(jid, attrs, true);
};
/**
......@@ -2095,13 +2089,13 @@ converse.plugins.add('converse-muc', {
*/
function autoJoinRooms () {
_converse.auto_join_rooms.forEach(groupchat => {
if (_.isString(groupchat)) {
if (isString(groupchat)) {
if (_converse.chatboxes.where({'jid': groupchat}).length) {
return;
}
_converse.api.rooms.open(groupchat);
} else if (_.isObject(groupchat)) {
_converse.api.rooms.open(groupchat.jid, _.clone(groupchat));
} else if (isObject(groupchat)) {
_converse.api.rooms.open(groupchat.jid, clone(groupchat));
} else {
_converse.log(
'Invalid groupchat criteria specified for "auto_join_rooms"',
......@@ -2187,13 +2181,13 @@ converse.plugins.add('converse-muc', {
* @returns {Promise} Promise which resolves with the Backbone.Model representing the chat.
*/
create (jids, attrs={}) {
attrs = _.isString(attrs) ? {'nick': attrs} : (attrs || {});
attrs = isString(attrs) ? {'nick': attrs} : (attrs || {});
if (!attrs.nick && _converse.muc_nickname_from_jid) {
attrs.nick = Strophe.getNodeFromJid(_converse.bare_jid);
}
if (jids === undefined) {
throw new TypeError('rooms.create: You need to provide at least one JID');
} else if (_.isString(jids)) {
} else if (isString(jids)) {
return createChatRoom(jids, attrs);
}
return jids.map(jid => createChatRoom(jid, attrs));
......@@ -2264,7 +2258,7 @@ converse.plugins.add('converse-muc', {
const err_msg = 'rooms.open: You need to provide at least one JID';
_converse.log(err_msg, Strophe.LogLevel.ERROR);
throw(new TypeError(err_msg));
} else if (_.isString(jids)) {
} else if (isString(jids)) {
const room = await _converse.api.rooms.create(jids, attrs);
room && room.maybeShow(force);
return room;
......@@ -2288,7 +2282,7 @@ converse.plugins.add('converse-muc', {
* the user's JID will be used.
* @param {boolean} create A boolean indicating whether the room should be created
* if not found (default: `false`)
* @returns {Promise} Promise which resolves with the Backbone.Model representing the chat.
* @returns { Promise<_converse.ChatRoom> }
* @example
* _converse.api.waitUntil('roomsAutoJoined').then(() => {
* const create_if_not_found = true;
......@@ -2299,28 +2293,26 @@ converse.plugins.add('converse-muc', {
* )
* });
*/
get (jids, attrs, create) {
if (_.isString(attrs)) {
attrs = {'nick': attrs};
} else if (attrs === undefined) {
attrs = {};
}
if (jids === undefined) {
const result = [];
_converse.chatboxes.each(function (chatbox) {
if (chatbox.get('type') === _converse.CHATROOMS_TYPE) {
result.push(chatbox);
async get (jids, attrs={}, create=false) {
async function _get (jid) {
let model = await _converse.api.chatboxes.get(jid);
if (!model && create) {
model = await _converse.api.chatboxes.create(jid, attrs, _converse.ChatRoom);
} else {
model = (model && model.get('type') === _converse.CHATROOMS_TYPE) ? model : null;
if (model && Object.keys(attrs).length) {
model.save(attrs);
}
});
return result;
}
if (!attrs.nick) {
attrs.nick = Strophe.getNodeFromJid(_converse.bare_jid);
return model;
}
if (_.isString(jids)) {
return getChatRoom(jids, attrs, create);
if (jids === undefined) {
const chats = await _converse.api.chatboxes.get();
return chats.filter(c => (c.get('type') === _converse.CHATROOMS_TYPE));
} else if (isString(jids)) {
return _get(jids);
}
return jids.map(jid => getChatRoom(jid, attrs, create));
return Promise.all(jids.map(jid => _get(jid)));
}
}
});
......
......@@ -6,9 +6,11 @@ import "./converse-bookmarks"; // XEP-0199 XMPP Ping
import "./converse-bosh"; // XEP-0206 BOSH
import "./converse-caps"; // XEP-0115 Entity Capabilities
import "./converse-chatboxes"; // Backbone Collection and Models for chat boxes
import "./converse-chat"; // Support for one-on-one chats
import "./converse-disco"; // XEP-0030 Service discovery
import "./converse-mam"; // XEP-0313 Message Archive Management
import "./converse-muc"; // XEP-0045 Multi-user chat
import "./converse-headlines"; // Support for headline messages
import "./converse-ping"; // XEP-0199 XMPP Ping
import "./converse-pubsub"; // XEP-0060 Pubsub
import "./converse-roster"; // Contacts Roster
......
......@@ -52,7 +52,7 @@
};
utils.openControlBox = async function (_converse) {
const model = await _converse.api.chats.open('controlbox');
const model = await _converse.api.controlbox.open();
await u.waitUntil(() => model.get('connected'));
var toggle = document.querySelector(".toggle-controlbox");
if (!u.isVisible(document.querySelector("#controlbox"))) {
......@@ -121,7 +121,7 @@
utils.openChatRoomViaModal = async function (_converse, jid, nick='') {
// Opens a new chatroom
const model = await _converse.api.chats.open('controlbox');
const model = await _converse.api.controlbox.open('controlbox');
await u.waitUntil(() => model.get('connected'));
utils.openControlBox();
const view = await _converse.chatboxviews.get('controlbox');
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment