From 4ca30c4b932383754beac8b8c0e0dd55d5b96ac0 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Wed, 10 Mar 2021 12:17:23 +0100 Subject: [PATCH] Create `converse-muc-chatarea` component --- sass/_chatrooms.scss | 7 + spec/chatbox.js | 4 +- spec/emojis.js | 16 +- spec/mam.js | 18 +- spec/me-messages.js | 2 +- spec/mentions.js | 2 +- spec/messages.js | 164 +++++++++--------- spec/muc.js | 42 ++--- src/headless/plugins/roster/contacts.js | 2 +- src/plugins/chatview/bottom_panel.js | 6 +- src/plugins/chatview/view.js | 18 +- src/plugins/headlines-view/view.js | 1 - src/plugins/minimize/utils.js | 5 +- src/plugins/muc-views/chatarea.js | 129 ++++++++++++++ src/plugins/muc-views/muc.js | 47 +---- .../muc-views/templates/muc-chatarea.js | 27 +++ src/plugins/muc-views/templates/muc.js | 18 +- src/shared/chat/baseview.js | 23 +-- 18 files changed, 327 insertions(+), 204 deletions(-) create mode 100644 src/plugins/muc-views/chatarea.js create mode 100644 src/plugins/muc-views/templates/muc-chatarea.js diff --git a/sass/_chatrooms.scss b/sass/_chatrooms.scss index 6b79ec948..19642e407 100644 --- a/sass/_chatrooms.scss +++ b/sass/_chatrooms.scss @@ -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; } diff --git a/spec/chatbox.js b/spec/chatbox.js index f161b8ba5..7db1f881f 100644 --- a/spec/chatbox.js +++ b/spec/chatbox.js @@ -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(); diff --git a/spec/emojis.js b/spec/emojis.js index a1bb79aea..878f3deab 100644 --- a/spec/emojis.js +++ b/spec/emojis.js @@ -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, '') === '😇'); 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); diff --git a/spec/mam.js b/spec/mam.js index 4e359c32d..747849838 100644 --- a/spec/mam.js +++ b/spec/mam.js @@ -267,8 +267,8 @@ describe("Message Archive Management", function () { `); _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 () { `); _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 () { `); _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 () { `); _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( ` @@ -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(); })) }); diff --git a/spec/me-messages.js b/spec/me-messages.js index 7a24db937..ae1357f1c 100644 --- a/spec/me-messages.js +++ b/spec/me-messages.js @@ -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(); diff --git a/spec/mentions.js b/spec/mentions.js index 97770f132..68d009c20 100644 --- a/spec/mentions.js +++ b/spec/mentions.js @@ -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 z3r0 gibson mr.robot, how are you?' ); diff --git a/spec/messages.js b/spec/messages.js index e4ec2a527..c3a9974fa 100644 --- a/spec/messages.js +++ b/spec/messages.js @@ -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 () { `); _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(` `); _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(` `); _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(` 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?\nhttps://conversejs.org'; }); 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); /* 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); diff --git a/spec/muc.js b/spec/muc.js index 0adb7baaf..41233c4ae 100644 --- a/spec/muc.js +++ b/spec/muc.js @@ -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( ` @@ -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 () { `); 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(/:.*$/, '')); diff --git a/src/headless/plugins/roster/contacts.js b/src/headless/plugins/roster/contacts.js index dd3f45f45..5cc474568 100644 --- a/src/headless/plugins/roster/contacts.js +++ b/src/headless/plugins/roster/contacts.js @@ -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); } diff --git a/src/plugins/chatview/bottom_panel.js b/src/plugins/chatview/bottom_panel.js index 37dec2a77..728f98391 100644 --- a/src/plugins/chatview/bottom_panel.js +++ b/src/plugins/chatview/bottom_panel.js @@ -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); diff --git a/src/plugins/chatview/view.js b/src/plugins/chatview/view.js index 6345f86d6..a87ac6f62 100644 --- a/src/plugins/chatview/view.js +++ b/src/plugins/chatview/view.js @@ -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` + + `, + this.help_container + ); + } + getHelpMessages () { // eslint-disable-line class-methods-use-this return [ `/clear: ${__('Remove messages')}`, diff --git a/src/plugins/headlines-view/view.js b/src/plugins/headlines-view/view.js index 2244b6c2d..d77cbaabe 100644 --- a/src/plugins/headlines-view/view.js +++ b/src/plugins/headlines-view/view.js @@ -52,7 +52,6 @@ class HeadlinesView extends BaseChatView { }) ); render(result, this); - this.content = this.querySelector('.chat-content'); return this; } diff --git a/src/plugins/minimize/utils.js b/src/plugins/minimize/utils.js index 993499f7d..7856a087c 100644 --- a/src/plugins/minimize/utils.js +++ b/src/plugins/minimize/utils.js @@ -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); /** diff --git a/src/plugins/muc-views/chatarea.js b/src/plugins/muc-views/chatarea.js new file mode 100644 index 000000000..11731d6ab --- /dev/null +++ b/src/plugins/muc-views/chatarea.js @@ -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 [ + `/admin: ${__("Change user's affiliation to admin")}`, + `/ban: ${__('Ban user by changing their affiliation to outcast')}`, + `/clear: ${__('Clear the chat area')}`, + `/close: ${__('Close this groupchat')}`, + `/deop: ${__('Change user role to participant')}`, + `/destroy: ${__('Remove this groupchat')}`, + `/help: ${__('Show this menu')}`, + `/kick: ${__('Kick user from groupchat')}`, + `/me: ${__('Write in 3rd person')}`, + `/member: ${__('Grant membership to a user')}`, + `/modtools: ${__('Opens up the moderator tools GUI')}`, + `/mute: ${__("Remove user's ability to post messages")}`, + `/nick: ${__('Change your nickname')}`, + `/op: ${__('Grant moderator role to user')}`, + `/owner: ${__('Grant ownership of this groupchat')}`, + `/register: ${__('Register your nickname')}`, + `/revoke: ${__("Revoke the user's current affiliation")}`, + `/subject: ${__('Set groupchat subject')}`, + `/topic: ${__('Set groupchat subject (alias for /subject)')}`, + `/voice: ${__('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); diff --git a/src/plugins/muc-views/muc.js b/src/plugins/muc-views/muc.js index 83bcc1544..e4423e0ea 100644 --- a/src/plugins/muc-views/muc.js +++ b/src/plugins/muc-views/muc.js @@ -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 [ - `/admin: ${__("Change user's affiliation to admin")}`, - `/ban: ${__('Ban user by changing their affiliation to outcast')}`, - `/clear: ${__('Clear the chat area')}`, - `/close: ${__('Close this groupchat')}`, - `/deop: ${__('Change user role to participant')}`, - `/destroy: ${__('Remove this groupchat')}`, - `/help: ${__('Show this menu')}`, - `/kick: ${__('Kick user from groupchat')}`, - `/me: ${__('Write in 3rd person')}`, - `/member: ${__('Grant membership to a user')}`, - `/modtools: ${__('Opens up the moderator tools GUI')}`, - `/mute: ${__("Remove user's ability to post messages")}`, - `/nick: ${__('Change your nickname')}`, - `/op: ${__('Grant moderator role to user')}`, - `/owner: ${__('Grant ownership of this groupchat')}`, - `/register: ${__('Register your nickname')}`, - `/revoke: ${__("Revoke the user's current affiliation")}`, - `/subject: ${__('Set groupchat subject')}`, - `/topic: ${__('Set groupchat subject (alias for /subject)')}`, - `/voice: ${__('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(); diff --git a/src/plugins/muc-views/templates/muc-chatarea.js b/src/plugins/muc-views/templates/muc-chatarea.js new file mode 100644 index 000000000..e744bb1c0 --- /dev/null +++ b/src/plugins/muc-views/templates/muc-chatarea.js @@ -0,0 +1,27 @@ +import { html } from "lit-html"; +import { _converse } from '@converse/headless/core'; + +export default (o) => html` +
+
+ + + ${o.show_help_messages ? html`
+
` : '' } +
+ +
+ + +`; diff --git a/src/plugins/muc-views/templates/muc.js b/src/plugins/muc-views/templates/muc.js index f4ba6bbf6..e0940db3b 100644 --- a/src/plugins/muc-views/templates/muc.js +++ b/src/plugins/muc-views/templates/muc.js @@ -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`
-
-
- - -
-
- -
- - -
+
`; diff --git a/src/shared/chat/baseview.js b/src/shared/chat/baseview.js index 02101e861..d1f3f9a50 100644 --- a/src/shared/chat/baseview.js +++ b/src/shared/chat/baseview.js @@ -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` - - `, - 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) {