Commit bbe2a622 authored by JC Brand's avatar JC Brand

converse-muc: Create `info` and `error` messages on the model

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