Create converse-muc-chatarea component

This commit is contained in:
JC Brand 2021-03-10 12:17:23 +01:00
parent 4fd848ff3e
commit 4ca30c4b93
18 changed files with 327 additions and 204 deletions

View File

@ -140,6 +140,13 @@
width: 100%;
overflow: hidden;
converse-muc-chatarea {
width: 100%;
display: flex;
flex-direction: row;
flex-flow: nowrap;
}
.row {
flex-direction: row;
}

View File

@ -37,7 +37,7 @@ describe("Chatboxes", function () {
id: u.getUniqueId()
}).c('body').t('hello world').tree();
await _converse.handleMessageStanza(msg);
await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
await u.waitUntil(() => view.querySelectorAll('.chat-msg').length);
const msg_txt_sel = 'converse-chat-message:last-child .chat-msg__body';
await u.waitUntil(() => view.querySelector(msg_txt_sel).textContent.trim() === 'hello world');
done();
@ -524,7 +524,7 @@ describe("Chatboxes", function () {
id: u.getUniqueId()
}).c('body').t('hello world').tree();
await _converse.handleMessageStanza(msg);
const msg_el = await u.waitUntil(() => view.content.querySelector('.chat-msg'));
const msg_el = await u.waitUntil(() => view.querySelector('.chat-msg'));
await u.waitUntil( () => view.querySelector('.chat-content__notifications').innerText === '');
expect(msg_el.querySelector('.chat-msg__text').textContent).toBe('hello world');
done();

View File

@ -267,7 +267,7 @@ describe("Emojis", function () {
await new Promise(resolve => _converse.on('chatBoxViewInitialized', resolve));
const view = _converse.api.chatviews.get(sender_jid);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
await u.waitUntil(() => u.hasClass('chat-msg__text--larger', view.content.querySelector('.chat-msg__text')));
await u.waitUntil(() => u.hasClass('chat-msg__text--larger', view.querySelector('.chat-msg__text')));
_converse.handleMessageStanza($msg({
'from': sender_jid,
@ -279,7 +279,7 @@ describe("Emojis", function () {
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 2);
let sel = '.message:last-child .chat-msg__text';
await u.waitUntil(() => u.hasClass('chat-msg__text--larger', view.content.querySelector(sel)));
await u.waitUntil(() => u.hasClass('chat-msg__text--larger', view.querySelector(sel)));
// Test that a modified message that no longer contains only
// emojis now renders normally again.
@ -293,7 +293,7 @@ describe("Emojis", function () {
});
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 3);
const last_msg_sel = 'converse-chat-message:last-child .chat-msg__text';
await u.waitUntil(() => view.content.querySelector(last_msg_sel).textContent === '💩 😇');
await u.waitUntil(() => view.querySelector(last_msg_sel).textContent === '💩 😇');
expect(textarea.value).toBe('');
bottom_panel.onKeyDown({
@ -314,7 +314,7 @@ describe("Emojis", function () {
await u.waitUntil(() => Array.from(view.querySelectorAll('.chat-msg__text'))
.filter(el => el.textContent === edited_text).length);
expect(view.model.messages.models.length).toBe(3);
let message = view.content.querySelector(last_msg_sel);
let message = view.querySelector(last_msg_sel);
expect(u.hasClass('chat-msg__text--larger', message)).toBe(false);
textarea.value = ':smile: Hello world!';
@ -333,7 +333,7 @@ describe("Emojis", function () {
});
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 5);
message = view.content.querySelector('.message:last-child .chat-msg__text');
message = view.querySelector('.message:last-child .chat-msg__text');
expect(u.hasClass('chat-msg__text--larger', message)).toBe(true);
done()
}));
@ -355,11 +355,11 @@ describe("Emojis", function () {
await new Promise(resolve => _converse.on('chatBoxViewInitialized', resolve));
const view = _converse.api.chatviews.get(contact_jid);
await new Promise(resolve => view.model.messages.once('rendered', resolve));
await u.waitUntil(() => view.content.querySelector('.chat-msg__text').innerHTML.replace(/<!---->/g, '') ===
await u.waitUntil(() => view.querySelector('.chat-msg__text').innerHTML.replace(/<!---->/g, '') ===
'<img class="emoji" draggable="false" title=":innocent:" alt="😇" src="https://twemoji.maxcdn.com/v/12.1.6//72x72/1f607.png">');
const last_msg_sel = 'converse-chat-message:last-child .chat-msg__text';
let message = view.content.querySelector(last_msg_sel);
let message = view.querySelector(last_msg_sel);
await u.waitUntil(() => u.isVisible(message.querySelector('.emoji')), 1000);
let imgs = message.querySelectorAll('.emoji');
expect(imgs.length).toBe(1);
@ -374,7 +374,7 @@ describe("Emojis", function () {
keyCode: 13 // Enter
});
await new Promise(resolve => view.model.messages.once('rendered', resolve));
message = view.content.querySelector(last_msg_sel);
message = view.querySelector(last_msg_sel);
await u.waitUntil(() => u.isVisible(message.querySelector('.emoji')), 1000);
imgs = message.querySelectorAll('.emoji');
expect(imgs.length).toBe(2);

View File

@ -267,8 +267,8 @@ describe("Message Archive Management", function () {
</iq>`);
_converse.connection._dataRecv(mock.createRequest(result));
await u.waitUntil(() => view.model.messages.length === 5);
await u.waitUntil(() => view.content.querySelectorAll('.chat-msg__text').length);
await u.waitUntil(() => Array.from(view.content.querySelectorAll('.chat-msg__text'))
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
await u.waitUntil(() => Array.from(view.querySelectorAll('.chat-msg__text'))
.map(e => e.textContent).join(' ') === "2nd Message 3rd Message 4th Message 5th Message 6th Message", 1000);
done();
}));
@ -455,7 +455,7 @@ describe("Message Archive Management", function () {
<stanza-id xmlns="urn:xmpp:sid:0" id="45fbbf2a-1059-479d-9283-c8effaf05621" by="trek-radio@conference.lightwitch.org"/>
</message>`);
_converse.connection._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
await u.waitUntil(() => view.querySelectorAll('.chat-msg').length);
expect(view.model.messages.length).toBe(1);
expect(view.model.messages.at(0).get('is_archived')).toBe(false);
expect(view.model.messages.at(0).get('stanza_id trek-radio@conference.lightwitch.org')).toBe('45fbbf2a-1059-479d-9283-c8effaf05621');
@ -480,7 +480,7 @@ describe("Message Archive Management", function () {
expect(view.model.getDuplicateMessage.calls.count()).toBe(1);
const result = view.model.getDuplicateMessage.calls.all()[0].returnValue
expect(result instanceof _converse.Message).toBe(true);
expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
expect(view.querySelectorAll('.chat-msg').length).toBe(1);
await u.waitUntil(() => view.model.updateMessage.calls.count());
expect(view.model.messages.length).toBe(1);
@ -502,7 +502,7 @@ describe("Message Archive Management", function () {
<stanza-id xmlns="urn:xmpp:sid:0" id="45fbbf2a-1059-479d-9283-c8effaf05621" by="trek-radio@conference.lightwitch.org"/>
</message>`);
_converse.connection._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
await u.waitUntil(() => view.querySelectorAll('.chat-msg').length);
// Not sure whether such a race-condition might pose a problem
// in "real-world" situations.
stanza = u.toStanza(
@ -524,7 +524,7 @@ describe("Message Archive Management", function () {
expect(view.model.getDuplicateMessage.calls.count()).toBe(1);
const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue
expect(result instanceof _converse.Message).toBe(true);
expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
expect(view.querySelectorAll('.chat-msg').length).toBe(1);
done();
}));
@ -550,8 +550,8 @@ describe("Message Archive Management", function () {
</result>
</message>`);
_converse.handleMAMResult(view.model, { 'messages': [stanza] });
await u.waitUntil(() => view.content.querySelectorAll('.chat-msg').length);
expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
await u.waitUntil(() => view.querySelectorAll('.chat-msg').length);
expect(view.querySelectorAll('.chat-msg').length).toBe(1);
stanza = u.toStanza(
`<message xmlns="jabber:client" to="romeo@montague.lit/orchard" from="discuss@conference.conversejs.org">
@ -574,7 +574,7 @@ describe("Message Archive Management", function () {
expect(view.model.getDuplicateMessage.calls.count()).toBe(1);
const result = await view.model.getDuplicateMessage.calls.all()[0].returnValue
expect(result instanceof _converse.Message).toBe(true);
expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
expect(view.querySelectorAll('.chat-msg').length).toBe(1);
done();
}))
});

View File

@ -22,7 +22,7 @@ describe("A Groupchat Message", function () {
'type': 'groupchat'
}).c('body').t(message).tree();
await view.model.handleMessageStanza(msg);
await u.waitUntil(() => sizzle('.chat-msg:last .chat-msg__text', view.content).pop());
await u.waitUntil(() => sizzle('.chat-msg:last .chat-msg__text', view).pop());
await u.waitUntil(() => view.querySelector('.chat-msg__text').textContent.trim() === 'is tired');
expect(view.querySelector('.chat-msg__author').textContent.includes('**Dyon van de Wege')).toBeTruthy();

View File

@ -381,7 +381,7 @@ describe("A sent groupchat message", function () {
const last_msg_sel = 'converse-chat-message:last-child .chat-msg__text';
await u.waitUntil(() =>
view.content.querySelector(last_msg_sel).innerHTML.replace(/<!---->/g, '') ===
view.querySelector(last_msg_sel).innerHTML.replace(/<!---->/g, '') ===
'hello <span class="mention">z3r0</span> <span class="mention">gibson</span> <span class="mention">mr.robot</span>, how are you?'
);

View File

@ -190,53 +190,53 @@ describe("A Chat Message", function () {
await u.waitUntil(() => view.querySelectorAll('.chat-msg').length === 7);
view.clearSpinner(); //cleanup
expect(view.content.querySelectorAll('.date-separator').length).toEqual(4);
expect(view.querySelectorAll('.date-separator').length).toEqual(4);
let day = sizzle('.date-separator:first', view.content).pop();
let day = sizzle('.date-separator:first', view).pop();
expect(day.getAttribute('data-isodate')).toEqual(dayjs('2017-12-31T00:00:00').toISOString());
let time = sizzle('time:first', view.content).pop();
let time = sizzle('time:first', view).pop();
expect(time.textContent).toEqual('Sunday Dec 31st 2017')
day = sizzle('.date-separator:first', view.content).pop();
day = sizzle('.date-separator:first', view).pop();
expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('Older message');
let el = sizzle('.chat-msg:first', view.content).pop().querySelector('.chat-msg__text')
let el = sizzle('.chat-msg:first', view).pop().querySelector('.chat-msg__text')
expect(u.hasClass('chat-msg--followup', el)).toBe(false);
expect(el.textContent).toEqual('Older message');
time = sizzle('time.separator-text:eq(1)', view.content).pop();
time = sizzle('time.separator-text:eq(1)', view).pop();
expect(time.textContent).toEqual("Monday Jan 1st 2018");
day = sizzle('.date-separator:eq(1)', view.content).pop();
day = sizzle('.date-separator:eq(1)', view).pop();
expect(day.getAttribute('data-isodate')).toEqual(dayjs('2018-01-01T00:00:00').toISOString());
expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('Inbetween message');
el = sizzle('.chat-msg:eq(1)', view.content).pop();
el = sizzle('.chat-msg:eq(1)', view).pop();
expect(el.querySelector('.chat-msg__text').textContent).toEqual('Inbetween message');
expect(el.parentElement.nextElementSibling.querySelector('.chat-msg__text').textContent).toEqual('another inbetween message');
el = sizzle('.chat-msg:eq(2)', view.content).pop();
el = sizzle('.chat-msg:eq(2)', view).pop();
expect(el.querySelector('.chat-msg__text').textContent)
.toEqual('another inbetween message');
expect(u.hasClass('chat-msg--followup', el)).toBe(true);
time = sizzle('time.separator-text:nth(2)', view.content).pop();
time = sizzle('time.separator-text:nth(2)', view).pop();
expect(time.textContent).toEqual("Tuesday Jan 2nd 2018");
day = sizzle('.date-separator:nth(2)', view.content).pop();
day = sizzle('.date-separator:nth(2)', view).pop();
expect(day.getAttribute('data-isodate')).toEqual(dayjs('2018-01-02T00:00:00').toISOString());
expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('An earlier message on the next day');
el = sizzle('.chat-msg:eq(3)', view.content).pop();
el = sizzle('.chat-msg:eq(3)', view).pop();
expect(el.querySelector('.chat-msg__text').textContent).toEqual('An earlier message on the next day');
expect(u.hasClass('chat-msg--followup', el)).toBe(false);
el = sizzle('.chat-msg:eq(4)', view.content).pop();
el = sizzle('.chat-msg:eq(4)', view).pop();
expect(el.querySelector('.chat-msg__text').textContent).toEqual('message');
expect(el.parentElement.nextElementSibling.querySelector('.chat-msg__text').textContent).toEqual('newer message from the next day');
expect(u.hasClass('chat-msg--followup', el)).toBe(false);
day = sizzle('.date-separator:last', view.content).pop();
day = sizzle('.date-separator:last', view).pop();
expect(day.getAttribute('data-isodate')).toEqual(dayjs().startOf('day').toISOString());
expect(day.nextElementSibling.querySelector('.chat-msg__text').textContent).toBe('latest message');
expect(u.hasClass('chat-msg--followup', el)).toBe(false);
@ -310,12 +310,12 @@ describe("A Chat Message", function () {
expect(msg_obj.get('sender')).toEqual('them');
expect(msg_obj.get('is_delayed')).toEqual(false);
// Now check that the message appears inside the chatbox in the DOM
await u.waitUntil(() => view.content.querySelector('.chat-msg .chat-msg__text'));
await u.waitUntil(() => view.querySelector('.chat-msg .chat-msg__text'));
expect(view.content.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(msgtext);
expect(view.content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
expect(view.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(msgtext);
expect(view.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
await u.waitUntil(() => chatbox.vcard.get('fullname') === 'Juliet Capulet')
expect(view.content.querySelector('span.chat-msg__author').textContent.trim()).toBe('Juliet Capulet');
expect(view.querySelector('span.chat-msg__author').textContent.trim()).toBe('Juliet Capulet');
done();
}));
@ -578,7 +578,7 @@ describe("A Chat Message", function () {
</message>`);
_converse.connection._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
expect(view.content.querySelector('.chat-msg__text').innerHTML.replace(/<!---->/g, '')).toBe('Hey\nHave you heard the news?');
expect(view.querySelector('.chat-msg__text').innerHTML.replace(/<!---->/g, '')).toBe('Hey\nHave you heard the news?');
stanza = u.toStanza(`
<message from="${contact_jid}"
type="chat"
@ -587,7 +587,7 @@ describe("A Chat Message", function () {
</message>`);
_converse.connection._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 2);
const text = view.content.querySelector('converse-chat-message:last-child .chat-msg__text').innerHTML.replace(/<!---->/g, '');
const text = view.querySelector('converse-chat-message:last-child .chat-msg__text').innerHTML.replace(/<!---->/g, '');
expect(text).toBe('Hey\n\u200B\nHave you heard the news?');
stanza = u.toStanza(`
<message from="${contact_jid}"
@ -597,7 +597,7 @@ describe("A Chat Message", function () {
</message>`);
_converse.connection._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 3);
expect(view.content.querySelector('converse-chat-message:last-child .chat-msg__text').innerHTML.replace(/<!---->/g, '')).toBe('Hey\nHave you heard\nthe news?');
expect(view.querySelector('converse-chat-message:last-child .chat-msg__text').innerHTML.replace(/<!---->/g, '')).toBe('Hey\nHave you heard\nthe news?');
stanza = u.toStanza(`
<message from="${contact_jid}"
@ -608,7 +608,7 @@ describe("A Chat Message", function () {
_converse.connection._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 4);
await u.waitUntil(() => {
const text = view.content.querySelector('converse-chat-message:last-child .chat-msg__text').innerHTML.replace(/<!---->/g, '');
const text = view.querySelector('converse-chat-message:last-child .chat-msg__text').innerHTML.replace(/<!---->/g, '');
return text === 'Hey\nHave you heard\n\u200B\nthe news?\n<a target="_blank" rel="noopener" href="https://conversejs.org/">https://conversejs.org</a>';
});
done();
@ -655,7 +655,7 @@ describe("A Chat Message", function () {
message = 'https://imgur.com/oxymPax';
await mock.sendMessage(view, message);
await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-image').length === 5, 1000);
expect(view.content.querySelectorAll('.chat-content .chat-image').length).toBe(5);
expect(view.querySelectorAll('.chat-content .chat-image').length).toBe(5);
// Check that the Imgur URL gets a .png attached to make it render
await u.waitUntil(() => Array.from(view.querySelectorAll('.chat-content .chat-image')).pop().src.endsWith('png'), 1000);
@ -681,7 +681,7 @@ describe("A Chat Message", function () {
await mock.sendMessage(view, message);
await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-msg').length === 2, 1000);
await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-image').length === 1, 1000)
expect(view.content.querySelectorAll('.chat-content .chat-image').length).toBe(1);
expect(view.querySelectorAll('.chat-content .chat-image').length).toBe(1);
done();
}));
@ -821,24 +821,24 @@ describe("A Chat Message", function () {
jasmine.clock().tick(1*ONE_MINUTE_LATER);
await mock.sendMessage(view, "Another message within 10 minutes, but from a different person");
expect(view.content.querySelectorAll('.message').length).toBe(6);
expect(view.content.querySelectorAll('.chat-msg').length).toBe(5);
expect(view.querySelectorAll('.message').length).toBe(6);
expect(view.querySelectorAll('.chat-msg').length).toBe(5);
const nth_child = (n) => `converse-chat-message:nth-child(${n}) .chat-msg`;
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(2)))).toBe(false);
expect(view.content.querySelector(`${nth_child(2)} .chat-msg__text`).textContent).toBe("A message");
expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(2)))).toBe(false);
expect(view.querySelector(`${nth_child(2)} .chat-msg__text`).textContent).toBe("A message");
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(3)))).toBe(true);
expect(view.content.querySelector(`${nth_child(3)} .chat-msg__text`).textContent).toBe(
expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(3)))).toBe(true);
expect(view.querySelector(`${nth_child(3)} .chat-msg__text`).textContent).toBe(
"Another message 3 minutes later");
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(4)))).toBe(false);
expect(view.content.querySelector(`${nth_child(4)} .chat-msg__text`).textContent).toBe(
expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(4)))).toBe(false);
expect(view.querySelector(`${nth_child(4)} .chat-msg__text`).textContent).toBe(
"Another message 14 minutes since we started");
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(5)))).toBe(true);
expect(view.content.querySelector(`${nth_child(5)} .chat-msg__text`).textContent).toBe(
expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(5)))).toBe(true);
expect(view.querySelector(`${nth_child(5)} .chat-msg__text`).textContent).toBe(
"Another message 1 minute and 1 second since the previous one");
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(6)))).toBe(false);
expect(view.content.querySelector(`${nth_child(6)} .chat-msg__text`).textContent).toBe(
expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(6)))).toBe(false);
expect(view.querySelector(`${nth_child(6)} .chat-msg__text`).textContent).toBe(
"Another message within 10 minutes, but from a different person");
// Let's add a delayed, inbetween message
@ -854,29 +854,29 @@ describe("A Chat Message", function () {
.tree());
await new Promise(resolve => view.model.messages.once('rendered', resolve));
expect(view.content.querySelectorAll('.message').length).toBe(7);
expect(view.content.querySelectorAll('.chat-msg').length).toBe(6);
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(2)))).toBe(false);
expect(view.content.querySelector(`${nth_child(2)} .chat-msg__text`).textContent).toBe("A message");
expect(view.querySelectorAll('.message').length).toBe(7);
expect(view.querySelectorAll('.chat-msg').length).toBe(6);
expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(2)))).toBe(false);
expect(view.querySelector(`${nth_child(2)} .chat-msg__text`).textContent).toBe("A message");
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(3)))).toBe(true);
expect(view.content.querySelector(`${nth_child(3)} .chat-msg__text`).textContent).toBe(
expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(3)))).toBe(true);
expect(view.querySelector(`${nth_child(3)} .chat-msg__text`).textContent).toBe(
"Another message 3 minutes later");
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(4)))).toBe(true);
expect(view.content.querySelector(`${nth_child(4)} .chat-msg__text`).textContent).toBe(
expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(4)))).toBe(true);
expect(view.querySelector(`${nth_child(4)} .chat-msg__text`).textContent).toBe(
"A delayed message, sent 5 minutes since we started");
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(5)))).toBe(true);
expect(view.content.querySelector(`${nth_child(5)} .chat-msg__text`).textContent).toBe(
expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(5)))).toBe(true);
expect(view.querySelector(`${nth_child(5)} .chat-msg__text`).textContent).toBe(
"Another message 14 minutes since we started");
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(6)))).toBe(true);
expect(view.content.querySelector(`${nth_child(6)} .chat-msg__text`).textContent).toBe(
expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(6)))).toBe(true);
expect(view.querySelector(`${nth_child(6)} .chat-msg__text`).textContent).toBe(
"Another message 1 minute and 1 second since the previous one");
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(7)))).toBe(false);
expect(view.content.querySelector(`${nth_child(7)} .chat-msg__text`).textContent).toBe(
expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(7)))).toBe(false);
expect(view.querySelector(`${nth_child(7)} .chat-msg__text`).textContent).toBe(
"Another message within 10 minutes, but from a different person");
_converse.handleMessageStanza(
@ -891,26 +891,26 @@ describe("A Chat Message", function () {
.tree());
await new Promise(resolve => view.model.messages.once('rendered', resolve));
expect(view.content.querySelectorAll('.chat-msg').length).toBe(7);
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(2)))).toBe(false);
expect(view.content.querySelector(`${nth_child(2)} .chat-msg__text`).textContent).toBe("A message");
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(3)))).toBe(true);
expect(view.content.querySelector(`${nth_child(3)} .chat-msg__text`).textContent).toBe(
expect(view.querySelectorAll('.chat-msg').length).toBe(7);
expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(2)))).toBe(false);
expect(view.querySelector(`${nth_child(2)} .chat-msg__text`).textContent).toBe("A message");
expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(3)))).toBe(true);
expect(view.querySelector(`${nth_child(3)} .chat-msg__text`).textContent).toBe(
"Another message 3 minutes later");
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(4)))).toBe(false);
expect(view.content.querySelector(`${nth_child(4)} .chat-msg__text`).textContent).toBe(
expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(4)))).toBe(false);
expect(view.querySelector(`${nth_child(4)} .chat-msg__text`).textContent).toBe(
"A carbon message 4 minutes later");
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(5)))).toBe(false);
expect(view.content.querySelector(`${nth_child(5)} .chat-msg__text`).textContent).toBe(
expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(5)))).toBe(false);
expect(view.querySelector(`${nth_child(5)} .chat-msg__text`).textContent).toBe(
"A delayed message, sent 5 minutes since we started");
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(6)))).toBe(true);
expect(view.content.querySelector(`${nth_child(6)} .chat-msg__text`).textContent).toBe(
expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(6)))).toBe(true);
expect(view.querySelector(`${nth_child(6)} .chat-msg__text`).textContent).toBe(
"Another message 14 minutes since we started");
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(7)))).toBe(true);
expect(view.content.querySelector(`${nth_child(7)} .chat-msg__text`).textContent).toBe(
expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(7)))).toBe(true);
expect(view.querySelector(`${nth_child(7)} .chat-msg__text`).textContent).toBe(
"Another message 1 minute and 1 second since the previous one");
expect(u.hasClass('chat-msg--followup', view.content.querySelector(nth_child(8)))).toBe(false);
expect(view.content.querySelector(`${nth_child(8)} .chat-msg__text`).textContent).toBe(
expect(u.hasClass('chat-msg--followup', view.querySelector(nth_child(8)))).toBe(false);
expect(view.querySelector(`${nth_child(8)} .chat-msg__text`).textContent).toBe(
"Another message within 10 minutes, but from a different person");
jasmine.clock().uninstall();
@ -1000,11 +1000,11 @@ describe("A Chat Message", function () {
expect(msg_obj.get('sender')).toEqual('them');
expect(msg_obj.get('is_delayed')).toEqual(false);
// Now check that the message appears inside the chatbox in the DOM
const mel = await u.waitUntil(() => view.content.querySelector('.chat-msg .chat-msg__text'));
const mel = await u.waitUntil(() => view.querySelector('.chat-msg .chat-msg__text'));
expect(mel.textContent).toEqual(message);
expect(view.content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
expect(view.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(view.content.querySelector('span.chat-msg__author').textContent.trim()).toBe('Mercutio');
expect(view.querySelector('span.chat-msg__author').textContent.trim()).toBe('Mercutio');
done();
}));
@ -1030,7 +1030,7 @@ describe("A Chat Message", function () {
expect(view.model.messages.length).toEqual(1);
const msg_obj = view.model.messages.at(0);
expect(msg_obj.get('message')).toEqual(message.trim());
const mel = await u.waitUntil(() => view.content.querySelector('.chat-msg .chat-msg__text'));
const mel = await u.waitUntil(() => view.querySelector('.chat-msg .chat-msg__text'));
expect(mel.textContent).toEqual(message.trim());
done();
}));
@ -1138,9 +1138,9 @@ describe("A Chat Message", function () {
await u.waitUntil(() => view.querySelector('.chat-msg__author').textContent.trim() === 'Mercutio');
// Now check that the message appears inside the chatbox in the DOM
expect(view.content.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(message);
expect(view.content.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
expect(view.content.querySelector('span.chat-msg__author').textContent.trim()).toBe('Mercutio');
expect(view.querySelector('.chat-msg .chat-msg__text').textContent).toEqual(message);
expect(view.querySelector('.chat-msg__time').textContent.match(/^[0-9][0-9]:[0-9][0-9]/)).toBeTruthy();
expect(view.querySelector('span.chat-msg__author').textContent.trim()).toBe('Mercutio');
done();
}));
});
@ -1174,7 +1174,7 @@ describe("A Chat Message", function () {
const view = _converse.api.chatviews.get(sender_jid);
const message = await view.model.sendMessage(msg_text);
await u.waitUntil(() => view.querySelectorAll('.chat-msg').length);
let msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.content).pop().textContent;
let msg_txt = sizzle('.chat-msg:last .chat-msg__text', view).pop().textContent;
expect(msg_txt).toEqual(msg_text);
// We send another message, for which an error will
@ -1182,8 +1182,8 @@ describe("A Chat Message", function () {
// after the relevant message.
msg_text = 'This message will be sent, and also receive an error';
const second_message = await view.model.sendMessage(msg_text);
await u.waitUntil(() => sizzle('.chat-msg .chat-msg__text', view.content).length === 2, 1000);
msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.content).pop().textContent;
await u.waitUntil(() => sizzle('.chat-msg .chat-msg__text', view).length === 2, 1000);
msg_txt = sizzle('.chat-msg:last .chat-msg__text', view).pop().textContent;
expect(msg_txt).toEqual(msg_text);
/* <message xmlns="jabber:client"
@ -1208,7 +1208,7 @@ describe("A Chat Message", function () {
.c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
.t('Server-to-server connection failed: Connecting failed: connection timeout');
_converse.connection._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() => view.content.querySelector('.chat-msg__error').textContent.trim() === error_txt);
await u.waitUntil(() => view.querySelector('.chat-msg__error').textContent.trim() === error_txt);
const other_error_txt = 'Server-to-server connection failed: Connecting failed: connection timeout';
stanza = $msg({
@ -1223,7 +1223,7 @@ describe("A Chat Message", function () {
.t(other_error_txt);
_converse.connection._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() =>
view.content.querySelector('converse-chat-message:last-child .chat-msg__error').textContent.trim() === other_error_txt);
view.querySelector('converse-chat-message:last-child .chat-msg__error').textContent.trim() === other_error_txt);
// We don't render duplicates
stanza = $msg({
@ -1237,11 +1237,11 @@ describe("A Chat Message", function () {
.c('text', { 'xmlns': "urn:ietf:params:xml:ns:xmpp-stanzas" })
.t('Server-to-server connection failed: Connecting failed: connection timeout');
_converse.connection._dataRecv(mock.createRequest(stanza));
expect(view.content.querySelectorAll('.chat-msg__error').length).toEqual(2);
expect(view.querySelectorAll('.chat-msg__error').length).toEqual(2);
msg_text = 'This message will be sent, and also receive an error';
const third_message = await view.model.sendMessage(msg_text);
await u.waitUntil(() => sizzle('converse-chat-message:last-child .chat-msg__text', view.content).pop()?.textContent === msg_text);
await u.waitUntil(() => sizzle('converse-chat-message:last-child .chat-msg__text', view).pop()?.textContent === msg_text);
// A different error message will however render
stanza = $msg({
@ -1302,7 +1302,7 @@ describe("A Chat Message", function () {
const msg_text = 'This message will show!';
await view.model.sendMessage(msg_text);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
expect(view.content.querySelectorAll('.chat-error').length).toEqual(0);
expect(view.querySelectorAll('.chat-error').length).toEqual(0);
done();
}));
});
@ -1317,7 +1317,7 @@ describe("A Chat Message", function () {
const view = _converse.api.chatviews.get(sender_jid);
// Create enough messages so that there's a scrollbar.
const promises = [];
view.content.scrollTop = 0;
view.querySelector('.chat-content').scrollTop = 0;
view.model.set('scrolled', true);
for (let i=0; i<20; i++) {
@ -1336,7 +1336,7 @@ describe("A Chat Message", function () {
expect(u.isVisible(indicator_el)).toBeTruthy();
expect(view.model.get('scrolled')).toBe(true);
expect(view.content.scrollTop).toBe(0);
expect(view.querySelector('.chat-content').scrollTop).toBe(0);
indicator_el.click();
expect(u.isVisible(indicator_el)).toBeFalsy();
expect(view.model.get('scrolled')).toBe(false);

View File

@ -927,7 +927,7 @@ describe("Groupchats", function () {
const num_info_msgs = await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-info').length);
expect(num_info_msgs).toBe(1);
expect(sizzle('div.chat-info', view.content).pop().textContent.trim()).toBe("This groupchat is not anonymous");
expect(sizzle('div.chat-info', view).pop().textContent.trim()).toBe("This groupchat is not anonymous");
const csntext = await u.waitUntil(() => view.querySelector('.chat-content__notifications').textContent);
expect(csntext.trim()).toEqual("some1 has entered the groupchat");
@ -1408,7 +1408,7 @@ describe("Groupchats", function () {
}).c('body').t('Some message').tree();
await view.model.handleMessageStanza(msg);
await u.waitUntil(() => sizzle('.chat-msg:last .chat-msg__text', view.content).pop());
await u.waitUntil(() => sizzle('.chat-msg:last .chat-msg__text', view).pop());
let stanza = u.toStanza(
`<presence xmlns="jabber:client" to="romeo@montague.lit/orchard" type="unavailable" from="conversations@conference.siacs.eu/Guus">
@ -2089,8 +2089,8 @@ describe("Groupchats", function () {
}).c('body').t(text);
await view.model.handleMessageStanza(message.nodeTree);
await u.waitUntil(() => view.querySelectorAll('.chat-msg').length);
expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
expect(view.content.querySelector('.chat-msg__text').textContent.trim()).toBe(text);
expect(view.querySelectorAll('.chat-msg').length).toBe(1);
expect(view.querySelector('.chat-msg__text').textContent.trim()).toBe(text);
expect(_converse.api.trigger).toHaveBeenCalledWith('message', jasmine.any(Object));
done();
}));
@ -2111,7 +2111,7 @@ describe("Groupchats", function () {
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
expect(_converse.api.trigger).toHaveBeenCalledWith('messageSend', jasmine.any(_converse.Message));
expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
expect(view.querySelectorAll('.chat-msg').length).toBe(1);
// Let's check that if we receive the same message again, it's
// not shown.
@ -2127,7 +2127,7 @@ describe("Groupchats", function () {
<origin-id xmlns="urn:xmpp:sid:0" id="${view.model.messages.at(0).get('origin_id')}"/>
</message>`);
await view.model.handleMessageStanza(stanza);
expect(view.content.querySelectorAll('.chat-msg').length).toBe(1);
expect(view.querySelectorAll('.chat-msg').length).toBe(1);
expect(sizzle('.chat-msg__text:last').pop().textContent.trim()).toBe(text);
expect(view.model.messages.length).toBe(1);
// We don't emit an event if it's our own message
@ -2158,7 +2158,8 @@ describe("Groupchats", function () {
await Promise.all(promises);
// Give enough time for `markScrolled` to have been called
setTimeout(async () => {
view.content.scrollTop = 0;
const content = view.querySelector('.chat-content');
content.scrollTop = 0;
await view.model.handleMessageStanza(
$msg({
from: 'lounge@montague.lit/someone',
@ -2168,9 +2169,9 @@ describe("Groupchats", function () {
}).c('body').t(message).tree());
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 21);
// Now check that the message appears inside the chatbox in the DOM
const msg_txt = sizzle('.chat-msg:last .chat-msg__text', view.content).pop().textContent;
const msg_txt = sizzle('.chat-msg:last .chat-msg__text', content).pop().textContent;
expect(msg_txt).toEqual(message);
expect(view.content.scrollTop).toBe(0);
expect(content.scrollTop).toBe(0);
done();
}, 500);
}));
@ -2346,8 +2347,8 @@ describe("Groupchats", function () {
_converse.connection._dataRecv(mock.createRequest(presence));
expect(view.model.session.get('connection_status')).toBe(converse.ROOMSTATUS.ENTERED);
expect(view.content.querySelectorAll('div.chat-info').length).toBe(1);
expect(sizzle('div.chat-info', view.content)[0].textContent.trim()).toBe(
expect(view.querySelectorAll('div.chat-info').length).toBe(1);
expect(sizzle('div.chat-info', view)[0].textContent.trim()).toBe(
__(_converse.muc.new_nickname_messages["303"], "newnick")
);
occupants = view.querySelector('.occupant-list');
@ -2964,7 +2965,7 @@ describe("Groupchats", function () {
bottom_panel.onKeyDown(enter);
await u.waitUntil(() => sizzle('converse-chat-help .chat-info', view).length);
const chat_help_el = view.querySelector('converse-chat-help');
let chat_help_el = view.querySelector('converse-chat-help');
let info_messages = sizzle('.chat-info', chat_help_el);
expect(info_messages.length).toBe(19);
expect(info_messages.pop().textContent.trim()).toBe('/voice: Allow muted user to post messages');
@ -2991,11 +2992,12 @@ describe("Groupchats", function () {
occupant.set('affiliation', 'admin');
view.querySelector('.close-chat-help').click();
await u.waitUntil(() => chat_help_el.hidden);
expect(view.model.get('show_help_messages')).toBe(false);
await u.waitUntil(() => view.querySelector('converse-chat-help') === null);
textarea.value = '/help';
bottom_panel.onKeyDown(enter);
await u.waitUntil(() => !chat_help_el.hidden);
chat_help_el = await u.waitUntil(() => view.querySelector('converse-chat-help'));
info_messages = sizzle('.chat-info', chat_help_el);
expect(info_messages.length).toBe(18);
let commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
@ -3006,18 +3008,18 @@ describe("Groupchats", function () {
]);
occupant.set('affiliation', 'member');
view.querySelector('.close-chat-help').click();
await u.waitUntil(() => chat_help_el.hidden);
await u.waitUntil(() => view.querySelector('converse-chat-help') === null);
textarea.value = '/help';
bottom_panel.onKeyDown(enter);
await u.waitUntil(() => !chat_help_el.hidden);
chat_help_el = await u.waitUntil(() => view.querySelector('converse-chat-help'));
info_messages = sizzle('.chat-info', chat_help_el);
expect(info_messages.length).toBe(9);
commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
expect(commands).toEqual(["/clear", "/help", "/kick", "/me", "/modtools", "/mute", "/nick", "/register", "/voice"]);
view.querySelector('.close-chat-help').click();
await u.waitUntil(() => chat_help_el.hidden);
await u.waitUntil(() => view.querySelector('converse-chat-help') === null);
expect(view.model.get('show_help_messages')).toBe(false);
occupant.set('role', 'participant');
@ -3026,7 +3028,7 @@ describe("Groupchats", function () {
textarea.value = '/help';
bottom_panel.onKeyDown(enter);
await u.waitUntil(() => view.model.get('show_help_messages'));
await u.waitUntil(() => !chat_help_el.hidden);
chat_help_el = await u.waitUntil(() => view.querySelector('converse-chat-help'));
info_messages = sizzle('.chat-info', chat_help_el);
expect(info_messages.length).toBe(5);
commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));
@ -3036,11 +3038,11 @@ describe("Groupchats", function () {
// Note: we're making a shortcut here, this value should never be set manually
view.model.config.set('changesubject', true);
view.querySelector('.close-chat-help').click();
await u.waitUntil(() => chat_help_el.hidden);
await u.waitUntil(() => view.querySelector('converse-chat-help') === null);
textarea.value = '/help';
bottom_panel.onKeyDown(enter);
await u.waitUntil(() => !chat_help_el.hidden, 1000);
chat_help_el = await u.waitUntil(() => view.querySelector('converse-chat-help'));
info_messages = sizzle('.chat-info', chat_help_el);
expect(info_messages.length).toBe(7);
commands = info_messages.map(m => m.textContent.replace(/:.*$/, ''));

View File

@ -73,7 +73,7 @@ const RosterContacts = Collection.extend({
if (u.isErrorObject(result)) {
log.error(result);
// Force a full roster refresh
_converse.session.set('roster_cached', false)
_converse.session.save('roster_cached', false)
this.data.save('version', undefined);
}

View File

@ -19,10 +19,10 @@ export default class ChatBottomPanel extends ElementView {
super.connectedCallback();
this.model = _converse.chatboxes.get(this.getAttribute('jid'));
this.listenTo(this.model, 'change:composing_spoiler', this.renderMessageForm);
await this.model.initialized;
this.listenTo(this.model.messages, 'change:correcting', this.onMessageCorrecting);
this.render();
api.listen.on('chatBoxScrolledDown', () => this.hideNewMessagesIndicator());
}
render () {
@ -79,6 +79,10 @@ export default class ChatBottomPanel extends ElementView {
this.renderToolbar();
}
hideNewMessagesIndicator () {
this.querySelector('.new-msgs-indicator')?.classList.add('hidden');
}
onMessageCorrecting (message) {
if (message.get('correcting')) {
this.insertIntoTextArea(u.prefixMentions(message), true, true);

View File

@ -1,10 +1,10 @@
import 'plugins/chatview/heading.js';
import 'plugins/chatview/bottom_panel.js';
import { html, render } from 'lit-html';
import BaseChatView from 'shared/chat/baseview.js';
import tpl_chat from './templates/chat.js';
import { __ } from 'i18n';
import { _converse, api, converse } from '@converse/headless/core';
import { render } from 'lit-html';
const u = converse.env.utils;
const { dayjs } = converse.env;
@ -58,11 +58,25 @@ export default class ChatView extends BaseChatView {
this.model.toJSON(), { 'markScrolled': ev => this.markScrolled(ev) })
);
render(result, this);
this.content = this.querySelector('.chat-content');
this.help_container = this.querySelector('.chat-content__help');
return this;
}
renderHelpMessages () {
render(
html`
<converse-chat-help
.model=${this.model}
.messages=${this.getHelpMessages()}
?hidden=${!this.model.get('show_help_messages')}
type="info"
chat_type="${this.model.get('type')}"
></converse-chat-help>
`,
this.help_container
);
}
getHelpMessages () { // eslint-disable-line class-methods-use-this
return [
`<strong>/clear</strong>: ${__('Remove messages')}`,

View File

@ -52,7 +52,6 @@ class HeadlinesView extends BaseChatView {
})
);
render(result, this);
this.content = this.querySelector('.chat-content');
return this;
}

View File

@ -190,10 +190,11 @@ function onMaximized (view) {
*/
function onMinimized (view) {
// save the scroll position to restore it on maximize
const content = view.querySelector('.chat-content__messages');
if (view.model.collection && view.model.collection.browserStorage) {
view.model.save({ 'scroll': view.content.scrollTop });
view.model.save({ 'scroll': content.scrollTop });
} else {
view.model.set({ 'scroll': view.content.scrollTop });
view.model.set({ 'scroll': content.scrollTop });
}
view.model.setChatState(_converse.INACTIVE);
/**

View File

@ -0,0 +1,129 @@
import debounce from 'lodash-es/debounce';
import tpl_muc_chatarea from './templates/muc-chatarea.js';
import { CustomElement } from 'components/element.js';
import { __ } from 'i18n';
import { _converse, api, converse } from '@converse/headless/core';
const { u } = converse.env;
export default class MUCChatArea extends CustomElement {
static get properties () {
return {
jid: { type: String },
show_help_messages: { type: Boolean },
type: { type: String },
}
}
connectedCallback () {
super.connectedCallback();
this.model = _converse.chatboxes.get(this.jid);
this.markScrolled = debounce(this._markScrolled, 100);
this.listenTo(this.model, 'change:show_help_messages', () => this.requestUpdate());
}
render () {
return tpl_muc_chatarea({
'help_messages': this.getHelpMessages(),
'jid': this.jid,
'model': this.model,
'occupants': this.model.occupants,
'show_help_messages': this.model.get('show_help_messages'),
'show_send_button': _converse.show_send_button,
'show_sidebar': this.shouldShowSidebar(),
'type': this.type,
});
}
shouldShowSidebar () {
return (
!this.model.get('hidden_occupants') &&
this.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED
);
}
getHelpMessages () {
const setting = api.settings.get('muc_disable_slash_commands');
const disabled_commands = Array.isArray(setting) ? setting : [];
return [
`<strong>/admin</strong>: ${__("Change user's affiliation to admin")}`,
`<strong>/ban</strong>: ${__('Ban user by changing their affiliation to outcast')}`,
`<strong>/clear</strong>: ${__('Clear the chat area')}`,
`<strong>/close</strong>: ${__('Close this groupchat')}`,
`<strong>/deop</strong>: ${__('Change user role to participant')}`,
`<strong>/destroy</strong>: ${__('Remove this groupchat')}`,
`<strong>/help</strong>: ${__('Show this menu')}`,
`<strong>/kick</strong>: ${__('Kick user from groupchat')}`,
`<strong>/me</strong>: ${__('Write in 3rd person')}`,
`<strong>/member</strong>: ${__('Grant membership to a user')}`,
`<strong>/modtools</strong>: ${__('Opens up the moderator tools GUI')}`,
`<strong>/mute</strong>: ${__("Remove user's ability to post messages")}`,
`<strong>/nick</strong>: ${__('Change your nickname')}`,
`<strong>/op</strong>: ${__('Grant moderator role to user')}`,
`<strong>/owner</strong>: ${__('Grant ownership of this groupchat')}`,
`<strong>/register</strong>: ${__('Register your nickname')}`,
`<strong>/revoke</strong>: ${__("Revoke the user's current affiliation")}`,
`<strong>/subject</strong>: ${__('Set groupchat subject')}`,
`<strong>/topic</strong>: ${__('Set groupchat subject (alias for /subject)')}`,
`<strong>/voice</strong>: ${__('Allow muted user to post messages')}`
]
.filter(line => disabled_commands.every(c => !line.startsWith(c + '<', 9)))
.filter(line => this.model.getAllowedCommands().some(c => line.startsWith(c + '<', 9)));
}
/**
* Called when the chat content is scrolled up or down.
* We want to record when the user has scrolled away from
* the bottom, so that we don't automatically scroll away
* from what the user is reading when new messages are received.
*
* Don't call this method directly, instead, call `markScrolled`,
* which debounces this method by 100ms.
* @private
*/
_markScrolled (ev) {
let scrolled = true;
let scrollTop = null;
const msgs_container = this.querySelector('.chat-content__messages');
const is_at_bottom =
msgs_container.scrollTop + msgs_container.clientHeight >= msgs_container.scrollHeight - 62; // sigh...
if (is_at_bottom) {
scrolled = false;
this.onScrolledDown();
} else if (msgs_container.scrollTop === 0) {
/**
* Triggered once the chat's message area has been scrolled to the top
* @event _converse#chatBoxScrolledUp
* @property { _converse.ChatBoxView | _converse.ChatRoomView } view
* @example _converse.api.listen.on('chatBoxScrolledUp', obj => { ... });
*/
api.trigger('chatBoxScrolledUp', this);
} else {
scrollTop = ev.target.scrollTop;
}
u.safeSave(this.model, { scrolled, scrollTop });
}
onScrolledDown () {
if (!this.model.isHidden()) {
this.model.clearUnreadMsgCounter();
// Clear location hash if set to one of the messages in our history
const hash = window.location.hash;
hash && this.model.messages.get(hash.slice(1)) && _converse.router.history.navigate();
}
/**
* Triggered once the chat's message area has been scrolled down to the bottom.
* @event _converse#chatBoxScrolledDown
* @type {object}
* @property { _converse.ChatBox | _converse.ChatRoom } chatbox - The chat model
* @example _converse.api.listen.on('chatBoxScrolledDown', obj => { ... });
*/
api.trigger('chatBoxScrolledDown', { 'chatbox': this.model });
}
}
api.elements.define('converse-muc-chatarea', MUCChatArea);

View File

@ -63,7 +63,6 @@ export default class MUCView extends BaseChatView {
await this.render();
// Need to be registered after render has been called.
this.listenTo(this.model, 'change:show_help_messages', this.renderHelpMessages);
this.listenTo(this.model.messages, 'add', this.onMessageAdded);
this.listenTo(this.model.occupants, 'change:show', this.showJoinOrLeaveNotification);
this.listenTo(this.model.occupants, 'remove', this.onOccupantRemoved);
@ -85,7 +84,8 @@ export default class MUCView extends BaseChatView {
this.setAttribute('id', this.model.get('box_id'));
render(
tpl_muc({
sidebar_hidden,
'chatview': this,
'conn_status': this.model.session.get('connection_status'),
'model': this.model,
'occupants': this.model.occupants,
'show_sidebar':
@ -93,13 +93,13 @@ export default class MUCView extends BaseChatView {
this.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED,
'markScrolled': ev => this.markScrolled(ev),
'muc_show_logs_before_join': api.settings.get('muc_show_logs_before_join'),
'show_send_button': _converse.show_send_button
'show_send_button': _converse.show_send_button,
sidebar_hidden,
}),
this
);
this.notifications = this.querySelector('.chat-content__notifications');
this.content = this.querySelector('.chat-content');
this.help_container = this.querySelector('.chat-content__help');
if (
@ -114,35 +114,6 @@ export default class MUCView extends BaseChatView {
!this.model.get('hidden') && this.show();
}
getHelpMessages () {
const setting = api.settings.get('muc_disable_slash_commands');
const disabled_commands = Array.isArray(setting) ? setting : [];
return [
`<strong>/admin</strong>: ${__("Change user's affiliation to admin")}`,
`<strong>/ban</strong>: ${__('Ban user by changing their affiliation to outcast')}`,
`<strong>/clear</strong>: ${__('Clear the chat area')}`,
`<strong>/close</strong>: ${__('Close this groupchat')}`,
`<strong>/deop</strong>: ${__('Change user role to participant')}`,
`<strong>/destroy</strong>: ${__('Remove this groupchat')}`,
`<strong>/help</strong>: ${__('Show this menu')}`,
`<strong>/kick</strong>: ${__('Kick user from groupchat')}`,
`<strong>/me</strong>: ${__('Write in 3rd person')}`,
`<strong>/member</strong>: ${__('Grant membership to a user')}`,
`<strong>/modtools</strong>: ${__('Opens up the moderator tools GUI')}`,
`<strong>/mute</strong>: ${__("Remove user's ability to post messages")}`,
`<strong>/nick</strong>: ${__('Change your nickname')}`,
`<strong>/op</strong>: ${__('Grant moderator role to user')}`,
`<strong>/owner</strong>: ${__('Grant ownership of this groupchat')}`,
`<strong>/register</strong>: ${__('Register your nickname')}`,
`<strong>/revoke</strong>: ${__("Revoke the user's current affiliation")}`,
`<strong>/subject</strong>: ${__('Set groupchat subject')}`,
`<strong>/topic</strong>: ${__('Set groupchat subject (alias for /subject)')}`,
`<strong>/voice</strong>: ${__('Allow muted user to post messages')}`
]
.filter(line => disabled_commands.every(c => !line.startsWith(c + '<', 9)))
.filter(line => this.model.getAllowedCommands().some(c => line.startsWith(c + '<', 9)));
}
onStartResizeOccupants (ev) {
this.resizing = true;
this.addEventListener('mousemove', this.onMouseMove);
@ -361,7 +332,7 @@ export default class MUCView extends BaseChatView {
renderNicknameForm () {
if (api.settings.get('muc_show_logs_before_join')) {
this.hideSpinner();
u.showElement(this.querySelector('.chat-area'));
u.showElement(this.querySelector('converse-muc-chatarea'));
} else {
const form = this.querySelector('.muc-nickname-form');
const tpl_result = tpl_muc_nickname_form(this.model.toJSON());
@ -443,8 +414,7 @@ export default class MUCView extends BaseChatView {
}
showDestroyedMessage () {
u.hideElement(this.querySelector('.chat-area'));
u.hideElement(this.querySelector('.occupants'));
u.hideElement(this.querySelector('converse-muc-chatarea'));
sizzle('.spinner', this).forEach(u.removeElement);
const reason = this.model.get('destroyed_reason');
@ -472,8 +442,7 @@ export default class MUCView extends BaseChatView {
if (!message) {
return;
}
u.hideElement(this.querySelector('.chat-area'));
u.hideElement(this.querySelector('.occupants'));
u.hideElement(this.querySelector('converse-muc-chatarea'));
sizzle('.spinner', this).forEach(u.removeElement);
const messages = [message];
@ -542,7 +511,7 @@ export default class MUCView extends BaseChatView {
} else if (conn_status === converse.ROOMSTATUS.ENTERED) {
this.hideSpinner();
this.hideChatRoomContents();
u.showElement(this.querySelector('.chat-area'));
u.showElement(this.querySelector('converse-muc-chatarea'));
this.querySelector('.occupants')?.setVisibility();
this.scrollDown();
this.maybeFocus();

View File

@ -0,0 +1,27 @@
import { html } from "lit-html";
import { _converse } from '@converse/headless/core';
export default (o) => html`
<div class="chat-area">
<div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
<converse-chat-content
class="chat-content__messages"
jid="${o.jid}"
@scroll=${o.markScrolled}></converse-chat-content>
${o.show_help_messages ? html`<div class="chat-content__help">
<converse-chat-help
.model=${o.model}
.messages=${o.help_messages}
?hidden=${!o.show_help_messages}
type="info"
chat_type="${_converse.CHATROOMS_TYPE}"
></converse-chat-help></div>` : '' }
</div>
<converse-muc-bottom-panel jid="${o.jid}" class="bottom-panel"></converse-muc-bottom-panel>
</div>
<div class="disconnect-container hidden"></div>
<converse-muc-sidebar class="occupants col-md-3 col-4 ${o.show_sidebar ? '' : 'hidden' }"
.occupants=${o.occupants}
.chatroom=${o.model}></converse-muc-sidebar>
`;

View File

@ -1,3 +1,4 @@
import '../chatarea.js';
import '../bottom_panel.js';
import '../heading.js';
import '../sidebar.js';
@ -8,22 +9,7 @@ export default (o) => html`
<converse-dragresize></converse-dragresize>
<converse-muc-heading jid="${o.model.get('jid')}" class="chat-head chat-head-chatroom row no-gutters"></converse-muc-heading>
<div class="chat-body chatroom-body row no-gutters">
<div class="chat-area col">
<div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
<converse-chat-content
class="chat-content__messages"
jid="${o.model.get('jid')}"
@scroll=${o.markScrolled}></converse-chat-content>
<div class="chat-content__help"></div>
</div>
<converse-muc-bottom-panel jid="${o.model.get('jid')}" class="bottom-panel"></converse-muc-bottom-panel>
</div>
<div class="disconnect-container hidden"></div>
<converse-muc-sidebar class="occupants col-md-3 col-4 ${o.sidebar_hidden ? 'hidden' : ''}"
.occupants=${o.occupants}
.chatroom=${o.model}></converse-muc-sidebar>
<div class="nickname-form-container"></div>
<converse-muc-chatarea jid="${o.model.get('jid')}"></converse-muc-chatarea>
</div>
</div>
`;

View File

@ -3,7 +3,6 @@ import log from '@converse/headless/log';
import tpl_spinner from 'templates/spinner.js';
import { ElementView } from '@converse/skeletor/src/element.js';
import { _converse, api, converse } from '@converse/headless/core';
import { html, render } from 'lit-html';
const u = converse.env.utils;
@ -14,21 +13,6 @@ export default class BaseChatView extends ElementView {
this.debouncedScrollDown = debounce(this.scrollDown, 100);
}
renderHelpMessages () {
render(
html`
<converse-chat-help
.model=${this.model}
.messages=${this.getHelpMessages()}
?hidden=${!this.model.get('show_help_messages')}
type="info"
chat_type="${this.model.get('type')}"
></converse-chat-help>
`,
this.help_container
);
}
hideNewMessagesIndicator () {
const new_msgs_indicator = this.querySelector('.new-msgs-indicator');
if (new_msgs_indicator !== null) {
@ -103,19 +87,20 @@ export default class BaseChatView extends ElementView {
}
addSpinner (append = false) {
const content = this.querySelector('.chat-content');
if (this.querySelector('.spinner') === null) {
const el = u.getElementFromTemplateResult(tpl_spinner());
if (append) {
this.content.insertAdjacentElement('beforeend', el);
content.insertAdjacentElement('beforeend', el);
this.scrollDown();
} else {
this.content.insertAdjacentElement('afterbegin', el);
content.insertAdjacentElement('afterbegin', el);
}
}
}
clearSpinner () {
this.content.querySelectorAll('.spinner').forEach(u.removeElement);
this.querySelectorAll('.chat-content .spinner').forEach(u.removeElement);
}
onStatusMessageChanged (item) {