Turn the bottom panel into a custom element

This commit is contained in:
JC Brand 2021-02-11 15:05:04 +01:00
parent 94bc087f50
commit 9ce4092a7c
30 changed files with 1333 additions and 1189 deletions

View File

@ -346,6 +346,10 @@
}
}
converse-muc-bottom-panel {
display: contents;
}
.muc-bottom-panel {
height: 3em;
padding: 0.5em;
@ -354,6 +358,11 @@
background-color: var(--chatroom-head-bg-color);
color: white;
&.muc-bottom-panel--muted {
height: 8em;
width: 100%;
}
&.muc-bottom-panel--nickname {
padding: 0;
height: 16em;

View File

@ -40,7 +40,7 @@ describe("The nickname autocomplete feature", function () {
await u.waitUntil(() => view.model.messages.last()?.get('received'));
// Test that pressing @ brings up all options
const textarea = view.querySelector('textarea.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
const at_event = {
'target': textarea,
'preventDefault': function preventDefault () {},
@ -48,9 +48,10 @@ describe("The nickname autocomplete feature", function () {
'keyCode': 50,
'key': '@'
};
view.onKeyDown(at_event);
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(at_event);
textarea.value = '@';
view.onKeyUp(at_event);
bottom_panel.onKeyUp(at_event);
await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 4);
expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick');
@ -93,7 +94,7 @@ describe("The nickname autocomplete feature", function () {
await u.waitUntil(() => view.model.messages.last()?.get('received'));
// Test that pressing @ brings up all options
const textarea = view.querySelector('textarea.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
const at_event = {
'target': textarea,
'preventDefault': function preventDefault () {},
@ -101,10 +102,11 @@ describe("The nickname autocomplete feature", function () {
'keyCode': 50,
'key': '@'
};
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
textarea.value = '\n'
view.onKeyDown(at_event);
bottom_panel.onKeyDown(at_event);
textarea.value = '\n@';
view.onKeyUp(at_event);
bottom_panel.onKeyUp(at_event);
await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 4);
expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick');
@ -148,7 +150,7 @@ describe("The nickname autocomplete feature", function () {
await u.waitUntil(() => view.model.messages.last()?.get('received'));
// Test that pressing @ brings up all options
const textarea = view.querySelector('textarea.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
const at_event = {
'target': textarea,
'preventDefault': function preventDefault () {},
@ -157,9 +159,10 @@ describe("The nickname autocomplete feature", function () {
'key': '@'
};
textarea.value = '('
view.onKeyDown(at_event);
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(at_event);
textarea.value = '(@';
view.onKeyUp(at_event);
bottom_panel.onKeyUp(at_event);
await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 4);
expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick');
@ -189,7 +192,7 @@ describe("The nickname autocomplete feature", function () {
})));
});
const textarea = view.querySelector('textarea.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
const at_event = {
'target': textarea,
'preventDefault': function preventDefault() { },
@ -198,10 +201,11 @@ describe("The nickname autocomplete feature", function () {
'key': '@'
};
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
// Test that results are sorted by query index
view.onKeyDown(at_event);
bottom_panel.onKeyDown(at_event);
textarea.value = '@ber';
view.onKeyUp(at_event);
bottom_panel.onKeyUp(at_event);
await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 3);
expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('bernard');
expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('naber');
@ -209,7 +213,7 @@ describe("The nickname autocomplete feature", function () {
// Test that when the query index is equal, results should be sorted by length
textarea.value = '@jo';
view.onKeyUp(at_event);
bottom_panel.onKeyUp(at_event);
await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 2);
expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('john');
expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('jones');
@ -235,7 +239,7 @@ describe("The nickname autocomplete feature", function () {
_converse.connection._dataRecv(mock.createRequest(presence));
expect(view.model.occupants.length).toBe(2);
const textarea = view.querySelector('textarea.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = "hello som";
// Press tab
@ -246,8 +250,9 @@ describe("The nickname autocomplete feature", function () {
'keyCode': 9,
'key': 'Tab'
}
view.onKeyDown(tab_event);
view.onKeyUp(tab_event);
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(tab_event);
bottom_panel.onKeyUp(tab_event);
await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false);
expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(1);
expect(view.querySelector('.suggestion-box__results li').textContent).toBe('some1');
@ -259,9 +264,9 @@ describe("The nickname autocomplete feature", function () {
}
for (var i=0; i<3; i++) {
// Press backspace 3 times to remove "som"
view.onKeyDown(backspace_event);
bottom_panel.onKeyDown(backspace_event);
textarea.value = textarea.value.slice(0, textarea.value.length-1)
view.onKeyUp(backspace_event);
bottom_panel.onKeyUp(backspace_event);
}
await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === true);
@ -278,8 +283,8 @@ describe("The nickname autocomplete feature", function () {
_converse.connection._dataRecv(mock.createRequest(presence));
textarea.value = "hello s s";
view.onKeyDown(tab_event);
view.onKeyUp(tab_event);
bottom_panel.onKeyDown(tab_event);
bottom_panel.onKeyUp(tab_event);
await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false);
expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(2);
@ -289,13 +294,13 @@ describe("The nickname autocomplete feature", function () {
'stopPropagation': function stopPropagation () {},
'keyCode': 38
}
view.onKeyDown(up_arrow_event);
view.onKeyUp(up_arrow_event);
bottom_panel.onKeyDown(up_arrow_event);
bottom_panel.onKeyUp(up_arrow_event);
expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(2);
expect(view.querySelector('.suggestion-box__results li[aria-selected="false"]').textContent).toBe('some1');
expect(view.querySelector('.suggestion-box__results li[aria-selected="true"]').textContent).toBe('some2');
view.onKeyDown({
bottom_panel.onKeyDown({
'target': textarea,
'preventDefault': function preventDefault () {},
'stopPropagation': function stopPropagation () {},
@ -316,12 +321,12 @@ describe("The nickname autocomplete feature", function () {
});
_converse.connection._dataRecv(mock.createRequest(presence));
textarea.value = "hello z";
view.onKeyDown(tab_event);
view.onKeyUp(tab_event);
bottom_panel.onKeyDown(tab_event);
bottom_panel.onKeyUp(tab_event);
await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false);
view.onKeyDown(tab_event);
view.onKeyUp(tab_event);
bottom_panel.onKeyDown(tab_event);
bottom_panel.onKeyUp(tab_event);
await u.waitUntil(() => textarea.value === 'hello @z3r0 ');
done();
}));
@ -345,7 +350,7 @@ describe("The nickname autocomplete feature", function () {
_converse.connection._dataRecv(mock.createRequest(presence));
expect(view.model.occupants.length).toBe(2);
const textarea = view.querySelector('textarea.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = "hello @some1 ";
// Press backspace
@ -356,9 +361,10 @@ describe("The nickname autocomplete feature", function () {
'keyCode': 8,
'key': 'Backspace'
}
view.onKeyDown(backspace_event);
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(backspace_event);
textarea.value = "hello @some1"; // Mimic backspace
view.onKeyUp(backspace_event);
bottom_panel.onKeyUp(backspace_event);
await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false);
expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(1);
expect(view.querySelector('.suggestion-box__results li').textContent).toBe('some1');

View File

@ -22,7 +22,6 @@ describe("Chatboxes", function () {
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
mock.sendMessage(view, '/help');
await u.waitUntil(() => sizzle('.chat-info:not(.chat-date)', view).length);
const info_messages = await u.waitUntil(() => sizzle('.chat-info:not(.chat-date)', view));
expect(info_messages.length).toBe(4);
@ -60,7 +59,8 @@ describe("Chatboxes", function () {
const textarea = view.querySelector('textarea.chat-textarea');
textarea.value = '/clear';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -279,13 +279,14 @@ describe("Chatboxes", function () {
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
};
view.onKeyDown(ev);
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
bottom_panel.onKeyDown(ev);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
view.onKeyUp(ev);
bottom_panel.onKeyUp(ev);
expect(counter.textContent).toBe('200');
textarea.value = 'hello world';
view.onKeyUp(ev);
bottom_panel.onKeyUp(ev);
expect(counter.textContent).toBe('189');
done();
}));
@ -430,7 +431,9 @@ describe("Chatboxes", function () {
expect(view.model.get('chat_state')).toBe('active');
spyOn(_converse.connection, 'send');
spyOn(_converse.api, "trigger").and.callThrough();
view.onKeyDown({
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
bottom_panel.onKeyDown({
target: view.querySelector('textarea.chat-textarea'),
keyCode: 1
});
@ -445,7 +448,7 @@ describe("Chatboxes", function () {
expect(stanza.childNodes[2].tagName).toBe('no-permanent-store');
// The notification is not sent again
view.onKeyDown({
bottom_panel.onKeyDown({
target: view.querySelector('textarea.chat-textarea'),
keyCode: 1
});
@ -469,7 +472,8 @@ describe("Chatboxes", function () {
expect(view.model.get('chat_state')).toBe('active');
spyOn(_converse.connection, 'send');
spyOn(_converse.api, "trigger").and.callThrough();
view.onKeyDown({
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
bottom_panel.onKeyDown({
target: view.querySelector('textarea.chat-textarea'),
keyCode: 1
});
@ -578,7 +582,8 @@ describe("Chatboxes", function () {
spyOn(_converse.connection, 'send');
spyOn(view.model, 'setChatState').and.callThrough();
expect(view.model.get('chat_state')).toBe('active');
view.onKeyDown({
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
bottom_panel.onKeyDown({
target: view.querySelector('textarea.chat-textarea'),
keyCode: 1
});
@ -602,14 +607,14 @@ describe("Chatboxes", function () {
// Test #359. A paused notification should not be sent
// out if the user simply types longer than the
// timeout.
view.onKeyDown({
bottom_panel.onKeyDown({
target: view.querySelector('textarea.chat-textarea'),
keyCode: 1
});
expect(view.model.setChatState).toHaveBeenCalled();
expect(view.model.get('chat_state')).toBe('composing');
view.onKeyDown({
bottom_panel.onKeyDown({
target: view.querySelector('textarea.chat-textarea'),
keyCode: 1
});
@ -697,7 +702,8 @@ describe("Chatboxes", function () {
let messages = await u.waitUntil(() => sent_stanzas.filter(s => s.matches('message')));
expect(messages.length).toBe(1);
expect(view.model.get('chat_state')).toBe('active');
view.onKeyDown({
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
bottom_panel.onKeyDown({
target: view.querySelector('textarea.chat-textarea'),
keyCode: 1
});
@ -924,18 +930,19 @@ describe("Chatboxes", function () {
await u.waitUntil(() => view.querySelector('.chat-msg'));
message = '/clear';
spyOn(view, 'clearMessages').and.callThrough();
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
spyOn(bottom_panel, 'clearMessages').and.callThrough();
spyOn(window, 'confirm').and.callFake(function () {
return true;
});
view.querySelector('.chat-textarea').value = message;
view.onKeyDown({
bottom_panel.onKeyDown({
target: view.querySelector('textarea.chat-textarea'),
preventDefault: function preventDefault () {},
keyCode: 13
});
expect(view.clearMessages.calls.all().length).toBe(1);
await view.clearMessages.calls.all()[0].returnValue;
expect(bottom_panel.clearMessages.calls.all().length).toBe(1);
await bottom_panel.clearMessages.calls.all()[0].returnValue;
expect(window.confirm).toHaveBeenCalled();
expect(view.model.messages.length, 0); // The messages must be removed from the chatbox
stored_messages = await view.model.messages.browserStorage.findAll();

View File

@ -15,14 +15,15 @@ describe("A Chat Message", function () {
const view = _converse.api.chatviews.get(contact_jid);
const textarea = view.querySelector('textarea.chat-textarea');
expect(textarea.value).toBe('');
view.onKeyDown({
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
keyCode: 38 // Up arrow
});
expect(textarea.value).toBe('');
textarea.value = 'But soft, what light through yonder airlock breaks?';
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -34,7 +35,7 @@ describe("A Chat Message", function () {
const first_msg = view.model.messages.findWhere({'message': 'But soft, what light through yonder airlock breaks?'});
expect(textarea.value).toBe('');
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
keyCode: 38 // Up arrow
});
@ -46,7 +47,7 @@ describe("A Chat Message", function () {
spyOn(_converse.connection, 'send');
let new_text = 'But soft, what light through yonder window breaks?';
textarea.value = new_text;
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -80,7 +81,7 @@ describe("A Chat Message", function () {
// Test that pressing the down arrow cancels message correction
await u.waitUntil(() => textarea.value === '')
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
keyCode: 38 // Up arrow
});
@ -89,7 +90,7 @@ describe("A Chat Message", function () {
expect(view.querySelectorAll('.chat-msg').length).toBe(1);
await u.waitUntil(() => u.hasClass('correcting', view.querySelector('.chat-msg')), 500);
expect(textarea.value).toBe('But soft, what light through yonder window breaks?');
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
keyCode: 40 // Down arrow
});
@ -100,7 +101,7 @@ describe("A Chat Message", function () {
new_text = 'It is the east, and Juliet is the one.';
textarea.value = new_text;
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -110,14 +111,14 @@ describe("A Chat Message", function () {
expect(view.querySelectorAll('.chat-msg').length).toBe(2);
textarea.value = 'Arise, fair sun, and kill the envious moon';
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
});
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 3);
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
keyCode: 38 // Up arrow
});
@ -129,7 +130,7 @@ describe("A Chat Message", function () {
textarea.selectionEnd = 0; // Happens by pressing up,
// but for some reason not in tests, so we set it manually.
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
keyCode: 38 // Up arrow
});
@ -140,7 +141,7 @@ describe("A Chat Message", function () {
await u.waitUntil(() => u.hasClass('correcting', sizzle('.chat-msg', view)[1]), 500);
textarea.value = 'It is the east, and Juliet is the sun.';
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -176,7 +177,8 @@ describe("A Chat Message", function () {
const textarea = view.querySelector('textarea.chat-textarea');
textarea.value = 'But soft, what light through yonder airlock breaks?';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -203,7 +205,7 @@ describe("A Chat Message", function () {
spyOn(_converse.connection, 'send');
textarea.value = 'But soft, what light through yonder window breaks?';
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -520,16 +522,17 @@ describe("A Groupchat Message", function () {
const muc_jid = 'lounge@montague.lit';
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
const view = _converse.api.chatviews.get(muc_jid);
const textarea = view.querySelector('textarea.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('textarea.chat-textarea'));
expect(textarea.value).toBe('');
view.onKeyDown({
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
keyCode: 38 // Up arrow
});
expect(textarea.value).toBe('');
textarea.value = 'But soft, what light through yonder airlock breaks?';
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -540,7 +543,7 @@ describe("A Groupchat Message", function () {
const first_msg = view.model.messages.findWhere({'message': 'But soft, what light through yonder airlock breaks?'});
expect(textarea.value).toBe('');
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
keyCode: 38 // Up arrow
});
@ -552,7 +555,7 @@ describe("A Groupchat Message", function () {
spyOn(_converse.connection, 'send');
const new_text = 'But soft, what light through yonder window breaks?'
textarea.value = new_text;
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -597,7 +600,7 @@ describe("A Groupchat Message", function () {
// Test that pressing the down arrow cancels message correction
expect(textarea.value).toBe('');
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
keyCode: 38 // Up arrow
});
@ -606,7 +609,7 @@ describe("A Groupchat Message", function () {
expect(view.querySelectorAll('.chat-msg').length).toBe(2);
await u.waitUntil(() => u.hasClass('correcting', view.querySelector('.chat-msg')), 500);
expect(textarea.value).toBe('But soft, what light through yonder window breaks?');
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
keyCode: 40 // Down arrow
});

View File

@ -48,7 +48,8 @@ describe("Emojis", function () {
'keyCode': 9,
'key': 'Tab'
}
view.onKeyDown(tab_event);
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(tab_event);
await u.waitUntil(() => view.querySelector('converse-emoji-picker .emoji-search').value === ':gri');
await u.waitUntil(() => sizzle('.emojis-lists__container--search .insert-emoji', view).length === 3, 1000);
let visible_emojis = sizzle('.emojis-lists__container--search .insert-emoji', view);
@ -88,7 +89,7 @@ describe("Emojis", function () {
_converse.connection._dataRecv(mock.createRequest(presence));
textarea.value = ':use';
view.onKeyDown(tab_event);
bottom_panel.onKeyDown(tab_event);
await u.waitUntil(() => u.isVisible(view.querySelector('.emoji-picker__lists')));
await u.waitUntil(() => input.value === ':use');
visible_emojis = sizzle('.insert-emoji:not(.hidden)', picker);
@ -114,7 +115,8 @@ describe("Emojis", function () {
'keyCode': 9,
'key': 'Tab'
}
view.onKeyDown(tab_event);
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(tab_event);
await u.waitUntil(() => u.isVisible(view.querySelector('.emoji-picker__lists')));
const picker = view.querySelector('converse-emoji-picker');
@ -132,7 +134,7 @@ describe("Emojis", function () {
emoji.click();
await u.waitUntil(() => textarea.value === ':grinning: ');
textarea.value = ':grinning: :';
view.onKeyDown(tab_event);
bottom_panel.onKeyDown(tab_event);
await u.waitUntil(() => input.value === ':');
input.value = ':grimacing';
@ -165,7 +167,8 @@ describe("Emojis", function () {
'key': 'Tab'
}
textarea.value = ':';
view.onKeyDown(tab_event);
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(tab_event);
await u.waitUntil(() => u.isVisible(view.querySelector('.emoji-picker__lists')));
const picker = view.querySelector('converse-emoji-picker');
const input = picker.querySelector('.emoji-search');
@ -176,7 +179,7 @@ describe("Emojis", function () {
expect(textarea.value).toBe(':100: ');
textarea.value = ':';
view.onKeyDown(tab_event);
bottom_panel.onKeyDown(tab_event);
await u.waitUntil(() => u.isVisible(view.querySelector('.emoji-picker__lists')));
await u.waitUntil(() => input.value === ':');
input.dispatchEvent(new KeyboardEvent('keydown', tab_event));
@ -282,7 +285,8 @@ describe("Emojis", function () {
// emojis now renders normally again.
const textarea = view.querySelector('textarea.chat-textarea');
textarea.value = ':poop: :innocent:';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -292,7 +296,7 @@ describe("Emojis", function () {
await u.waitUntil(() => view.content.querySelector(last_msg_sel).textContent === '💩 😇');
expect(textarea.value).toBe('');
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
keyCode: 38 // Up arrow
});
@ -302,7 +306,7 @@ describe("Emojis", function () {
await u.waitUntil(() => u.hasClass('correcting', view.querySelector(sel)), 500);
const edited_text = textarea.value += 'This is no longer an emoji-only message';
textarea.value = edited_text;
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -314,7 +318,7 @@ describe("Emojis", function () {
expect(u.hasClass('chat-msg__text--larger', message)).toBe(false);
textarea.value = ':smile: Hello world!';
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -322,7 +326,7 @@ describe("Emojis", function () {
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 4);
textarea.value = ':smile: :smiley: :imp:';
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -363,7 +367,8 @@ describe("Emojis", function () {
const textarea = view.querySelector('textarea.chat-textarea');
textarea.value = ':poop: :innocent:';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -414,7 +419,8 @@ describe("Emojis", function () {
const textarea = view.querySelector('textarea.chat-textarea');
textarea.value = 'Running tests for :converse:';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter

View File

@ -123,9 +123,10 @@ describe("A XEP-0333 Chat Marker", function () {
const muc_jid = 'lounge@montague.lit';
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
const view = _converse.api.chatviews.get(muc_jid);
const textarea = view.querySelector('textarea.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = 'But soft, what light through yonder airlock breaks?';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter

View File

@ -306,7 +306,7 @@ describe("A sent groupchat message", function () {
})));
await u.waitUntil(() => view.model.occupants.length === 2);
const textarea = view.querySelector('textarea.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = 'hello @Link Mauve'
const enter_event = {
'target': textarea,
@ -315,9 +315,10 @@ describe("A sent groupchat message", function () {
'keyCode': 13 // Enter
}
spyOn(_converse.connection, 'send');
view.onKeyDown(enter_event);
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter_event);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
const msg = _converse.connection.send.calls.all()[1].args[0];
const msg = _converse.connection.send.calls.all()[0].args[0];
expect(msg.toLocaleString())
.toBe(`<message from="romeo@montague.lit/orchard" id="${msg.nodeTree.getAttribute("id")}" `+
`to="lounge@montague.lit" type="groupchat" `+
@ -365,7 +366,7 @@ describe("A sent groupchat message", function () {
});
await u.waitUntil(() => view.model.occupants.length === 5);
const textarea = view.querySelector('textarea.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('textarea.chat-textarea'));
textarea.value = 'hello @z3r0 @gibson @mr.robot, how are you?'
const enter_event = {
'target': textarea,
@ -374,7 +375,8 @@ describe("A sent groupchat message", function () {
'keyCode': 13 // Enter
}
spyOn(_converse.connection, 'send');
view.onKeyDown(enter_event);
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter_event);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
const last_msg_sel = 'converse-chat-message:last-child .chat-msg__text';
@ -383,7 +385,7 @@ describe("A sent groupchat message", function () {
'hello <span class="mention">z3r0</span> <span class="mention">gibson</span> <span class="mention">mr.robot</span>, how are you?'
);
const msg = _converse.connection.send.calls.all()[1].args[0];
const msg = _converse.connection.send.calls.all()[0].args[0];
expect(msg.toLocaleString())
.toBe(`<message from="romeo@montague.lit/orchard" id="${msg.nodeTree.getAttribute("id")}" `+
`to="lounge@montague.lit" type="groupchat" `+
@ -404,14 +406,14 @@ describe("A sent groupchat message", function () {
expect(view.model.messages.at(0).get('correcting')).toBe(true);
expect(view.querySelectorAll('.chat-msg').length).toBe(1);
await u.waitUntil(() => u.hasClass('correcting', view.querySelector('.chat-msg')), 500);
await u.waitUntil(() => _converse.connection.send.calls.count() === 2);
await u.waitUntil(() => _converse.connection.send.calls.count() === 1);
textarea.value = 'hello @z3r0 @gibson @sw0rdf1sh, how are you?';
view.onKeyDown(enter_event);
bottom_panel.onKeyDown(enter_event);
await u.waitUntil(() => view.querySelector('.chat-msg__text').textContent ===
'hello z3r0 gibson sw0rdf1sh, how are you?', 500);
const correction = _converse.connection.send.calls.all()[2].args[0];
const correction = _converse.connection.send.calls.all()[1].args[0];
expect(correction.toLocaleString())
.toBe(`<message from="romeo@montague.lit/orchard" id="${correction.nodeTree.getAttribute("id")}" `+
`to="lounge@montague.lit" type="groupchat" `+
@ -449,7 +451,7 @@ describe("A sent groupchat message", function () {
await u.waitUntil(() => view.model.occupants.length === 5);
spyOn(_converse.connection, 'send');
const textarea = view.querySelector('textarea.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = 'hello @z3r0 @gibson @mr.robot, how are you?'
const enter_event = {
'target': textarea,
@ -457,7 +459,8 @@ describe("A sent groupchat message", function () {
'stopPropagation': function stopPropagation () {},
'keyCode': 13 // Enter
}
view.onKeyDown(enter_event);
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter_event);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
const msg = _converse.connection.send.calls.all()[1].args[0];
@ -483,7 +486,7 @@ describe("A sent groupchat message", function () {
const muc_jid = 'lounge@montague.lit';
await mock.openAndEnterChatRoom(_converse, muc_jid, 'tom', [], members);
const view = _converse.api.chatviews.get(muc_jid);
const textarea = view.querySelector('textarea.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = "Welcome @gibson 💩 We have a guide on how to do that here: https://conversejs.org/docs/html/index.html";
const enter_event = {
'target': textarea,
@ -491,7 +494,8 @@ describe("A sent groupchat message", function () {
'stopPropagation': function stopPropagation () {},
'keyCode': 13 // Enter
}
view.onKeyDown(enter_event);
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter_event);
const message = await u.waitUntil(() => view.querySelector('.chat-msg__text'));
expect(message.innerHTML.replace(/<!---->/g, '')).toEqual(
`Welcome <span class="mention">gibson</span> <span title=":poop:">💩</span> `+

View File

@ -527,7 +527,7 @@ describe("A Chat Message", function () {
const view = _converse.api.chatviews.get(contact_jid);
const message = 'This message contains a hyperlink: www.opkode.com';
spyOn(view.model, 'sendMessage').and.callThrough();
mock.sendMessage(view, message);
await mock.sendMessage(view, message);
expect(view.model.sendMessage).toHaveBeenCalled();
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view).pop();
@ -547,7 +547,7 @@ describe("A Chat Message", function () {
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.api.chatviews.get(contact_jid);
let message = 'This message contains a hyperlink with forbidden query params: https://www.opkode.com/?id=0&utm_content=1&utm_medium=2&s=1';
mock.sendMessage(view, message);
await mock.sendMessage(view, message);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
let msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view).pop();
await u.waitUntil(() => msg.innerHTML.replace(/<!---->/g, '') ===
@ -556,10 +556,10 @@ describe("A Chat Message", function () {
// Test assigning a string to filter_url_query_params
_converse.api.settings.set('filter_url_query_params', 'utm_medium');
message = 'Another message with a hyperlink with forbidden query params: https://www.opkode.com/?id=0&utm_content=1&utm_medium=2&s=1';
mock.sendMessage(view, message);
await mock.sendMessage(view, message);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 2);
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view).pop();
expect(msg.textContent).toEqual('Another message with a hyperlink with forbidden query params: https://www.opkode.com/?id=0&utm_content=1&s=1');
expect(msg.textContent).toEqual(message);
await u.waitUntil(() => msg.innerHTML.replace(/<!---->/g, '') ===
'Another message with a hyperlink with forbidden query params: '+
'<a target="_blank" rel="noopener" href="https://www.opkode.com/?id=0&amp;utm_content=1&amp;s=1">https://www.opkode.com/?id=0&amp;utm_content=1&amp;s=1</a>');
@ -622,7 +622,7 @@ describe("A Chat Message", function () {
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.api.chatviews.get(contact_jid);
spyOn(view.model, 'sendMessage').and.callThrough();
mock.sendMessage(view, message);
await mock.sendMessage(view, message);
await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-image').length, 1000)
expect(view.model.sendMessage).toHaveBeenCalled();
let msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop();
@ -632,7 +632,7 @@ describe("A Chat Message", function () {
`</a>`);
message += "?param1=val1&param2=val2";
mock.sendMessage(view, message);
await mock.sendMessage(view, message);
await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-image').length === 2, 1000);
expect(view.model.sendMessage).toHaveBeenCalled();
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop();
@ -643,7 +643,7 @@ describe("A Chat Message", function () {
// Test now with two images in one message
message += ' hello world '+base_url+"/logo/conversejs-filled.svg";
mock.sendMessage(view, message);
await mock.sendMessage(view, message);
await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-image').length === 4, 1000);
expect(view.model.sendMessage).toHaveBeenCalled();
msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop();
@ -653,7 +653,7 @@ describe("A Chat Message", function () {
// Configured image URLs are rendered
_converse.api.settings.set('image_urls_regex', /^https?:\/\/(?:www.)?(?:imgur\.com\/\w{7})\/?$/i);
message = 'https://imgur.com/oxymPax';
mock.sendMessage(view, message);
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);
@ -674,11 +674,11 @@ describe("A Chat Message", function () {
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.api.chatviews.get(contact_jid);
spyOn(view.model, 'sendMessage').and.callThrough();
mock.sendMessage(view, message);
await mock.sendMessage(view, message);
await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-msg').length === 1);
message = base_url+"/logo/conversejs-filled.svg";
mock.sendMessage(view, message);
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);
@ -698,7 +698,7 @@ describe("A Chat Message", function () {
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.api.chatviews.get(contact_jid);
spyOn(view.model, 'sendMessage').and.callThrough();
mock.sendMessage(view, message);
await mock.sendMessage(view, message);
await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-image').length, 1000)
expect(view.model.sendMessage).toHaveBeenCalled();
const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text').pop();
@ -721,7 +721,7 @@ describe("A Chat Message", function () {
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.api.chatviews.get(contact_jid);
spyOn(view.model, 'sendMessage').and.callThrough();
mock.sendMessage(view, message);
await mock.sendMessage(view, message);
expect(view.model.sendMessage).toHaveBeenCalled();
await u.waitUntil(() => view.querySelector('.chat-content .chat-msg'), 1000);
const msg = view.querySelector('.chat-content .chat-msg .chat-msg__text');

View File

@ -235,7 +235,7 @@ describe("A Minimized ChatBoxView's Unread Message Count", function () {
await mock.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
spyOn(view.model, 'sendMessage').and.callThrough();
mock.sendMessage(view, message);
await mock.sendMessage(view, message);
await u.waitUntil(() => view.querySelectorAll('.chat-content .chat-msg').length, 1000);
expect(view.model.sendMessage).toHaveBeenCalled();
const msg = sizzle('.chat-content .chat-msg:last .chat-msg__text', view).pop();

View File

@ -438,10 +438,12 @@ window.addEventListener('converse-loaded', () => {
.c('active', {'xmlns': Strophe.NS.CHATSTATES}).tree();
}
mock.sendMessage = function (view, message) {
mock.sendMessage = async function (view, message) {
const promise = new Promise(resolve => view.model.messages.once('rendered', resolve));
view.querySelector('.chat-textarea').value = message;
view.onKeyDown({
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = message;
const bottom_panel = view.querySelector('converse-chat-bottom-panel') || view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown({
target: view.querySelector('textarea.chat-textarea'),
preventDefault: () => {},
keyCode: 13

View File

@ -8,10 +8,11 @@ const u = converse.env.utils;
async function openModtools (_converse, view) {
const textarea = view.querySelector('.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/modtools';
const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 };
view.onKeyDown(enter);
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter);
await u.waitUntil(() => view.showModeratorToolsModal.calls.count());
const modal = _converse.api.modal.get('converse-modtools-modal');
await u.waitUntil(() => u.isVisible(modal.el), 1000);
@ -257,10 +258,11 @@ describe("The groupchat moderator tool", function () {
));
await u.waitUntil(() => (view.model.occupants.length === 7), 1000);
const textarea = view.querySelector('.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/modtools';
const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 };
view.onKeyDown(enter);
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter);
await u.waitUntil(() => view.showModeratorToolsModal.calls.count());
const modal = _converse.api.modal.get('converse-modtools-modal');
@ -460,10 +462,11 @@ describe("The groupchat moderator tool", function () {
const members = [{'jid': 'romeo@montague.lit', 'nick': 'romeo', 'affiliation': 'owner'}];
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', [], members);
const view = _converse.chatboxviews.get(muc_jid);
const textarea = view.querySelector('.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/modtools';
const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 };
view.onKeyDown(enter);
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter);
await u.waitUntil(() => view.showModeratorToolsModal.calls.count());
const modal = _converse.api.modal.get('converse-modtools-modal');

View File

@ -386,7 +386,8 @@ describe("Groupchats", function () {
await u.waitUntil(() => view.querySelector(sel)?.textContent.trim());
expect(view.querySelector(sel).textContent.trim()).toBe('Hello world')
view.querySelector('[name="nick"]').value = nick;
const nick_input = await u.waitUntil(() => view.querySelector('[name="nick"]'));
nick_input.value = nick;
view.querySelector('.muc-nickname-form input[type="submit"]').click();
_converse.connection.IQ_stanzas = [];
await mock.getRoomFeatures(_converse, muc_jid);
@ -2074,10 +2075,7 @@ describe("Groupchats", function () {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
spyOn(_converse.api, "trigger").and.callThrough();
const view = _converse.chatboxviews.get('lounge@montague.lit');
if (!view.querySelectorAll('.chat-area').length) {
view.renderChatArea();
}
var nick = mock.chatroom_names[0];
const nick = mock.chatroom_names[0];
view.model.occupants.create({
'nick': nick,
'muc_jid': `${view.model.get('jid')}/${nick}`
@ -2101,13 +2099,11 @@ describe("Groupchats", function () {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
spyOn(_converse.api, "trigger").and.callThrough();
const view = _converse.chatboxviews.get('lounge@montague.lit');
if (!view.querySelectorAll('.chat-area').length) {
view.renderChatArea();
}
const text = 'This is a sent message';
const textarea = view.querySelector('.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = text;
view.onKeyDown({
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13
@ -2961,10 +2957,11 @@ describe("Groupchats", function () {
spyOn(window, 'confirm').and.callFake(() => true);
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
const view = _converse.chatboxviews.get('lounge@montague.lit');
let textarea = view.querySelector('.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 };
textarea.value = '/help';
view.onKeyDown(enter);
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter);
await u.waitUntil(() => sizzle('converse-chat-help .chat-info', view).length);
const chat_help_el = view.querySelector('converse-chat-help');
@ -2997,7 +2994,7 @@ describe("Groupchats", function () {
await u.waitUntil(() => chat_help_el.hidden);
textarea.value = '/help';
view.onKeyDown(enter);
bottom_panel.onKeyDown(enter);
await u.waitUntil(() => !chat_help_el.hidden);
info_messages = sizzle('.chat-info', chat_help_el);
expect(info_messages.length).toBe(18);
@ -3012,7 +3009,7 @@ describe("Groupchats", function () {
await u.waitUntil(() => chat_help_el.hidden);
textarea.value = '/help';
view.onKeyDown(enter);
bottom_panel.onKeyDown(enter);
await u.waitUntil(() => !chat_help_el.hidden);
info_messages = sizzle('.chat-info', chat_help_el);
expect(info_messages.length).toBe(9);
@ -3025,10 +3022,9 @@ describe("Groupchats", function () {
occupant.set('role', 'participant');
// Role changes causes rerender, so we need to get the new textarea
textarea = view.querySelector('.chat-textarea');
textarea.value = '/help';
view.onKeyDown(enter);
bottom_panel.onKeyDown(enter);
await u.waitUntil(() => view.model.get('show_help_messages'));
await u.waitUntil(() => !chat_help_el.hidden);
info_messages = sizzle('.chat-info', chat_help_el);
@ -3043,7 +3039,7 @@ describe("Groupchats", function () {
await u.waitUntil(() => chat_help_el.hidden);
textarea.value = '/help';
view.onKeyDown(enter);
bottom_panel.onKeyDown(enter);
await u.waitUntil(() => !chat_help_el.hidden, 1000);
info_messages = sizzle('.chat-info', chat_help_el);
expect(info_messages.length).toBe(7);
@ -3057,13 +3053,14 @@ describe("Groupchats", function () {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
const view = _converse.chatboxviews.get('lounge@montague.lit');
var textarea = view.querySelector('.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
const enter = { 'target': textarea, 'preventDefault': function () {}, 'keyCode': 13 };
spyOn(window, 'confirm').and.callFake(() => true);
textarea.value = '/clear';
view.onKeyDown(enter);
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter);
textarea.value = '/help';
view.onKeyDown(enter);
bottom_panel.onKeyDown(enter);
await u.waitUntil(() => sizzle('.chat-info:not(.chat-event)', view).length);
const info_messages = sizzle('.chat-info:not(.chat-event)', view);
@ -3110,7 +3107,7 @@ describe("Groupchats", function () {
_converse.connection._dataRecv(mock.createRequest(presence));
expect(view.model.occupants.length).toBe(2);
const textarea = view.querySelector('.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
let sent_stanza;
spyOn(_converse.connection, 'send').and.callFake((stanza) => {
sent_stanza = stanza;
@ -3119,7 +3116,8 @@ describe("Groupchats", function () {
// First check that an error message appears when a
// non-existent nick is used.
textarea.value = '/member chris Welcome to the club!';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13
@ -3131,7 +3129,7 @@ describe("Groupchats", function () {
// Now test with an existing nick
textarea.value = '/member marc Welcome to the club!';
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13
@ -3234,15 +3232,15 @@ describe("Groupchats", function () {
it("takes /topic to set the groupchat topic", mock.initConverse([], {}, async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
const view = _converse.chatboxviews.get('lounge@montague.lit');
spyOn(view, 'clearMessages');
let sent_stanza;
spyOn(_converse.connection, 'send').and.callFake(function (stanza) {
sent_stanza = stanza;
});
// Check the alias /topic
const textarea = view.querySelector('.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/topic This is the groupchat subject';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13
@ -3252,7 +3250,7 @@ describe("Groupchats", function () {
// Check /subject
textarea.value = '/subject This is a new subject';
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13
@ -3266,7 +3264,7 @@ describe("Groupchats", function () {
// Check case insensitivity
textarea.value = '/Subject This is yet another subject';
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13
@ -3279,7 +3277,7 @@ describe("Groupchats", function () {
// Check unsetting the topic
textarea.value = '/topic';
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13
@ -3294,15 +3292,16 @@ describe("Groupchats", function () {
it("takes /clear to clear messages", mock.initConverse([], {}, async function (done, _converse) {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
const view = _converse.chatboxviews.get('lounge@montague.lit');
spyOn(view, 'clearMessages');
const textarea = view.querySelector('.chat-textarea')
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/clear';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
spyOn(bottom_panel, 'clearMessages');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13
});
expect(view.clearMessages).toHaveBeenCalled();
expect(bottom_panel.clearMessages).toHaveBeenCalled();
done();
}));
@ -3317,7 +3316,7 @@ describe("Groupchats", function () {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
const view = _converse.chatboxviews.get('lounge@montague.lit');
spyOn(view.model, 'setAffiliation').and.callThrough();
spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
spyOn(view.model, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
let presence = $pres({
'from': 'lounge@montague.lit/annoyingGuy',
@ -3332,14 +3331,15 @@ describe("Groupchats", function () {
});
_converse.connection._dataRecv(mock.createRequest(presence));
var textarea = view.querySelector('.chat-textarea')
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/owner';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13
});
expect(view.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled();
expect(view.model.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled();
const err_msg = await u.waitUntil(() => view.querySelector('.chat-error'));
expect(err_msg.textContent.trim()).toBe(
"Error: the \"owner\" command takes two arguments, the user's nickname and optionally a reason.");
@ -3349,7 +3349,7 @@ describe("Groupchats", function () {
// again via triggering Event doesn't work for some weird
// reason.
textarea.value = '/owner nobody You\'re responsible';
view.onFormSubmitted(new Event('submit'));
bottom_panel.onFormSubmitted(new Event('submit'));
await u.waitUntil(() => view.querySelectorAll('.chat-error').length === 2);
expect(Array.from(view.querySelectorAll('.chat-error')).pop().textContent.trim()).toBe(
"Error: couldn't find a groupchat participant based on your arguments");
@ -3361,9 +3361,9 @@ describe("Groupchats", function () {
// again via triggering Event doesn't work for some weird
// reason.
textarea.value = '/owner annoyingGuy You\'re responsible';
view.onFormSubmitted(new Event('submit'));
bottom_panel.onFormSubmitted(new Event('submit'));
expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3);
expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3);
expect(view.model.setAffiliation).toHaveBeenCalled();
// Check that the member list now gets updated
expect(Strophe.serialize(sent_IQ)).toBe(
@ -3405,7 +3405,7 @@ describe("Groupchats", function () {
await mock.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo');
const view = _converse.chatboxviews.get('lounge@montague.lit');
spyOn(view.model, 'setAffiliation').and.callThrough();
spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
spyOn(view.model, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
let presence = $pres({
'from': 'lounge@montague.lit/annoyingGuy',
@ -3420,14 +3420,15 @@ describe("Groupchats", function () {
});
_converse.connection._dataRecv(mock.createRequest(presence));
const textarea = view.querySelector('.chat-textarea')
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/ban';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13
});
expect(view.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled();
expect(view.model.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled();
await u.waitUntil(() => view.querySelector('.message:last-child')?.textContent?.trim() ===
"Error: the \"ban\" command takes two arguments, the user's nickname and optionally a reason.");
@ -3437,9 +3438,9 @@ describe("Groupchats", function () {
// again via triggering Event doesn't work for some weird
// reason.
textarea.value = '/ban annoyingGuy You\'re annoying';
view.onFormSubmitted(new Event('submit'));
bottom_panel.onFormSubmitted(new Event('submit'));
expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2);
expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2);
expect(view.model.setAffiliation).toHaveBeenCalled();
// Check that the member list now gets updated
expect(Strophe.serialize(sent_IQ)).toBe(
@ -3483,7 +3484,7 @@ describe("Groupchats", function () {
_converse.connection._dataRecv(mock.createRequest(presence));
textarea.value = '/ban joe22';
view.onFormSubmitted(new Event('submit'));
bottom_panel.onFormSubmitted(new Event('submit'));
await u.waitUntil(() => view.querySelector('converse-chat-message:last-child')?.textContent?.trim() ===
"Error: couldn't find a groupchat participant based on your arguments");
done();
@ -3502,7 +3503,7 @@ describe("Groupchats", function () {
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
const view = _converse.api.chatviews.get(muc_jid);
spyOn(view.model, 'setRole').and.callThrough();
spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
spyOn(view.model, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
let presence = $pres({
'from': 'lounge@montague.lit/annoying guy',
@ -3517,14 +3518,15 @@ describe("Groupchats", function () {
});
_converse.connection._dataRecv(mock.createRequest(presence));
const textarea = view.querySelector('.chat-textarea')
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/kick';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13
});
expect(view.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled();
expect(view.model.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled();
await u.waitUntil(() => view.querySelector('.message:last-child')?.textContent?.trim() ===
"Error: the \"kick\" command takes two arguments, the user's nickname and optionally a reason.");
expect(view.model.setRole).not.toHaveBeenCalled();
@ -3533,9 +3535,9 @@ describe("Groupchats", function () {
// again via triggering Event doesn't work for some weird
// reason.
textarea.value = '/kick @annoying guy You\'re annoying';
view.onFormSubmitted(new Event('submit'));
bottom_panel.onFormSubmitted(new Event('submit'));
expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2);
expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2);
expect(view.model.setRole).toHaveBeenCalled();
expect(Strophe.serialize(sent_IQ)).toBe(
`<iq id="${IQ_id}" to="lounge@montague.lit" type="set" xmlns="jabber:client">`+
@ -3591,7 +3593,7 @@ describe("Groupchats", function () {
IQ_id = sendIQ.bind(this)(iq, callback, errback);
});
spyOn(view.model, 'setRole').and.callThrough();
spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
spyOn(view.model, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
// New user enters the groupchat
/* <presence
@ -3618,15 +3620,16 @@ describe("Groupchats", function () {
await u.waitUntil(() => view.querySelector('.chat-content__notifications').textContent.trim() ===
"romeo and trustworthyguy have entered the groupchat");
const textarea = view.querySelector('.chat-textarea')
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/op';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13
});
expect(view.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled();
expect(view.model.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled();
await u.waitUntil(() => view.querySelector('.message:last-child')?.textContent?.trim() ===
"Error: the \"op\" command takes two arguments, the user's nickname and optionally a reason.");
@ -3636,9 +3639,9 @@ describe("Groupchats", function () {
// again via triggering Event doesn't work for some weird
// reason.
textarea.value = '/op trustworthyguy You\'re trustworthy';
view.onFormSubmitted(new Event('submit'));
bottom_panel.onFormSubmitted(new Event('submit'));
expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2);
expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2);
expect(view.model.setRole).toHaveBeenCalled();
expect(Strophe.serialize(sent_IQ)).toBe(
`<iq id="${IQ_id}" to="lounge@montague.lit" type="set" xmlns="jabber:client">`+
@ -3680,9 +3683,9 @@ describe("Groupchats", function () {
// again via triggering Event doesn't work for some weird
// reason.
textarea.value = '/deop trustworthyguy Perhaps not';
view.onFormSubmitted(new Event('submit'));
bottom_panel.onFormSubmitted(new Event('submit'));
expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3);
expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3);
expect(view.model.setRole).toHaveBeenCalled();
expect(Strophe.serialize(sent_IQ)).toBe(
`<iq id="${IQ_id}" to="lounge@montague.lit" type="set" xmlns="jabber:client">`+
@ -3730,7 +3733,7 @@ describe("Groupchats", function () {
IQ_id = sendIQ.bind(this)(iq, callback, errback);
});
spyOn(view.model, 'setRole').and.callThrough();
spyOn(view, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
spyOn(view.model, 'validateRoleOrAffiliationChangeArgs').and.callThrough();
// New user enters the groupchat
/* <presence
@ -3757,15 +3760,16 @@ describe("Groupchats", function () {
await u.waitUntil(() => view.querySelector('.chat-content__notifications').textContent.trim() ===
"romeo and annoyingGuy have entered the groupchat");
const textarea = view.querySelector('.chat-textarea')
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/mute';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13
});
expect(view.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled();
expect(view.model.validateRoleOrAffiliationChangeArgs).toHaveBeenCalled();
await u.waitUntil(() => view.querySelector('.message:last-child')?.textContent?.trim() ===
"Error: the \"mute\" command takes two arguments, the user's nickname and optionally a reason.");
expect(view.model.setRole).not.toHaveBeenCalled();
@ -3774,9 +3778,9 @@ describe("Groupchats", function () {
// again via triggering Event doesn't work for some weird
// reason.
textarea.value = '/mute annoyingGuy You\'re annoying';
view.onFormSubmitted(new Event('submit'));
bottom_panel.onFormSubmitted(new Event('submit'));
expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2);
expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2);
expect(view.model.setRole).toHaveBeenCalled();
expect(Strophe.serialize(sent_IQ)).toBe(
`<iq id="${IQ_id}" to="lounge@montague.lit" type="set" xmlns="jabber:client">`+
@ -3815,9 +3819,9 @@ describe("Groupchats", function () {
// again via triggering Event doesn't work for some weird
// reason.
textarea.value = '/voice annoyingGuy Now you can talk again';
view.onFormSubmitted(new Event('submit'));
bottom_panel.onFormSubmitted(new Event('submit'));
expect(view.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3);
expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3);
expect(view.model.setRole).toHaveBeenCalled();
expect(Strophe.serialize(sent_IQ)).toBe(
`<iq id="${IQ_id}" to="lounge@montague.lit" type="set" xmlns="jabber:client">`+
@ -3861,9 +3865,10 @@ describe("Groupchats", function () {
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
let view = _converse.api.chatviews.get(muc_jid);
spyOn(_converse.api, 'confirm').and.callThrough();
let textarea = view.querySelector('.chat-textarea');
let textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/destroy';
view.onFormSubmitted(new Event('submit'));
let bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onFormSubmitted(new Event('submit'));
let modal = await u.waitUntil(() => document.querySelector('.modal-dialog'));
await u.waitUntil(() => u.isVisible(modal));
@ -3899,8 +3904,8 @@ describe("Groupchats", function () {
'from': view.model.get('jid'),
'to': _converse.connection.jid
});
spyOn(_converse.api, "trigger").and.callThrough();
expect(_converse.chatboxes.length).toBe(2);
spyOn(_converse.api, "trigger").and.callThrough();
_converse.connection._dataRecv(mock.createRequest(result_stanza));
await u.waitUntil(() => (view.model.session.get('connection_status') === converse.ROOMSTATUS.DISCONNECTED));
await u.waitUntil(() => _converse.chatboxes.length === 1);
@ -3911,9 +3916,10 @@ describe("Groupchats", function () {
sent_IQs = _converse.connection.IQ_stanzas;
await mock.openAndEnterChatRoom(_converse, new_muc_jid, 'romeo');
view = _converse.api.chatviews.get(new_muc_jid);
textarea = view.querySelector('.chat-textarea');
textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/destroy';
view.onFormSubmitted(new Event('submit'));
bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onFormSubmitted(new Event('submit'));
modal = await u.waitUntil(() => document.querySelector('.modal-dialog'));
await u.waitUntil(() => u.isVisible(modal));
@ -5194,9 +5200,10 @@ describe("Groupchats", function () {
const muc_jid = 'trollbox@montague.lit';
await mock.openAndEnterChatRoom(_converse, muc_jid, 'troll');
const view = _converse.api.chatviews.get(muc_jid);
const textarea = view.querySelector('.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('textarea.chat-textarea'));
textarea.value = 'Hello world';
view.onFormSubmitted(new Event('submit'));
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onFormSubmitted(new Event('submit'));
await new Promise(resolve => view.model.messages.once('rendered', resolve));
let stanza = u.toStanza(`
@ -5213,7 +5220,7 @@ describe("Groupchats", function () {
"Your message was not delivered because you weren't allowed to send it.");
textarea.value = 'Hello again';
view.onFormSubmitted(new Event('submit'));
bottom_panel.onFormSubmitted(new Event('submit'));
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 2);
stanza = u.toStanza(`
@ -5248,7 +5255,7 @@ describe("Groupchats", function () {
const muc_jid = 'trollbox@montague.lit';
await mock.openAndEnterChatRoom(_converse, muc_jid, 'troll', features);
const view = _converse.api.chatviews.get(muc_jid);
expect(_.isNull(view.querySelector('.chat-textarea'))).toBe(false);
await u.waitUntil(() => view.querySelector('.chat-textarea'));
let stanza = u.toStanza(`
<presence
@ -5271,12 +5278,12 @@ describe("Groupchats", function () {
// the textarea becomes visible when the room's
// configuration changes to be non-moderated
view.model.features.set('moderated', false);
expect(view.querySelector('.muc-bottom-panel')).toBe(null);
let textarea = view.querySelector('.chat-textarea');
await u.waitUntil(() => view.querySelector('.muc-bottom-panel') === null);
const textarea = await u.waitUntil(() => view.querySelector('textarea.chat-textarea'));
expect(textarea === null).toBe(false);
view.model.features.set('moderated', true);
expect(view.querySelector('.chat-textarea')).toBe(null);
await u.waitUntil(() => view.querySelector('.chat-textarea') === null);
bottom_panel = view.querySelector('.muc-bottom-panel');
expect(bottom_panel.textContent.trim()).toBe("You're not allowed to send messages in this room");
@ -5299,10 +5306,7 @@ describe("Groupchats", function () {
</presence>`);
_converse.connection._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() => view.querySelector('.muc-bottom-panel') === null);
textarea = view.querySelector('.chat-textarea');
expect(textarea === null).toBe(false);
// Check now that things get restored when the user is given a voice
await u.waitUntil(() => view.querySelector('.chat-content__notifications').textContent.trim() === "troll has been given a voice");
done();

View File

@ -17,7 +17,7 @@ describe("A Groupchat Message", function () {
const muc_jid = 'lounge@montague.lit';
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
const view = _converse.api.chatviews.get(muc_jid);
const textarea = view.querySelector('textarea.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = 'hello world'
const enter_event = {
'target': textarea,
@ -25,7 +25,8 @@ describe("A Groupchat Message", function () {
'stopPropagation': function stopPropagation () {},
'keyCode': 13 // Enter
}
view.onKeyDown(enter_event);
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter_event);
await new Promise(resolve => view.model.messages.once('rendered', resolve));
const msg = view.model.messages.at(0);
@ -510,9 +511,10 @@ describe("A Groupchat Message", function () {
const muc_jid = 'lounge@montague.lit';
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
const view = _converse.api.chatviews.get(muc_jid);
const textarea = view.querySelector('textarea.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = 'But soft, what light through yonder airlock breaks?';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -584,9 +586,10 @@ describe("A Groupchat Message", function () {
const muc_jid = 'lounge@montague.lit';
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo');
const view = _converse.api.chatviews.get(muc_jid);
const textarea = view.querySelector('textarea.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = 'But soft, what light through yonder airlock breaks?';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter

View File

@ -112,7 +112,8 @@ describe("The OMEMO module", function() {
const textarea = view.querySelector('.chat-textarea');
textarea.value = 'This message will be encrypted';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -238,7 +239,7 @@ describe("The OMEMO module", function() {
const view = _converse.chatboxviews.get('lounge@montague.lit');
await u.waitUntil(() => initializedOMEMO(_converse));
const toolbar = view.querySelector('.chat-toolbar');
const toolbar = await u.waitUntil(() => view.querySelector('.chat-toolbar'));
const el = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo'));
el.click();
expect(view.model.get('omemo_active')).toBe(true);
@ -293,7 +294,8 @@ describe("The OMEMO module", function() {
const textarea = view.querySelector('.chat-textarea');
textarea.value = 'This message will be encrypted';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -457,7 +459,8 @@ describe("The OMEMO module", function() {
const textarea = view.querySelector('.chat-textarea');
textarea.value = 'This is an encrypted message from this device';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -504,15 +507,16 @@ describe("The OMEMO module", function() {
}).tree();
_converse.connection._dataRecv(mock.createRequest(stanza));
const toolbar = view.querySelector('.chat-toolbar');
const toolbar = await u.waitUntil(() => view.querySelector('.chat-toolbar'));
const toggle = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo'));
toggle.click();
expect(view.model.get('omemo_active')).toBe(true);
expect(view.model.get('omemo_supported')).toBe(true);
const textarea = view.querySelector('.chat-textarea');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = 'This message will be encrypted';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -1229,7 +1233,8 @@ describe("The OMEMO module", function() {
const textarea = view.querySelector('.chat-textarea');
textarea.value = 'This message will be sent encrypted';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13
@ -1274,7 +1279,7 @@ describe("The OMEMO module", function() {
const view = _converse.chatboxviews.get('lounge@montague.lit');
await u.waitUntil(() => initializedOMEMO(_converse));
const toolbar = view.querySelector('.chat-toolbar');
const toolbar = await u.waitUntil(() => view.querySelector('.chat-toolbar'));
let toggle = await u.waitUntil(() => toolbar.querySelector('.toggle-omemo'));
expect(view.model.get('omemo_active')).toBe(undefined);
expect(view.model.get('omemo_supported')).toBe(true);

View File

@ -110,7 +110,8 @@ describe("A delivery receipt", function () {
const view = _converse.chatboxviews.get(contact_jid);
const textarea = view.querySelector('textarea.chat-textarea');
textarea.value = 'But soft, what light through yonder airlock breaks?';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -131,7 +132,7 @@ describe("A delivery receipt", function () {
// Also handle receipts with type 'chat'. See #1353
spyOn(_converse, 'handleMessageStanza').and.callThrough();
textarea.value = 'Another message';
view.onKeyDown({
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter

View File

@ -17,9 +17,10 @@ describe("Chatrooms", function () {
const muc_jid = 'coven@chat.shakespeare.lit';
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo')
const view = _converse.chatboxviews.get(muc_jid);
const textarea = view.querySelector('.chat-textarea')
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/register';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13

View File

@ -112,7 +112,8 @@ describe("A spoiler message", function () {
const textarea = view.querySelector('.chat-textarea');
textarea.value = 'This is the spoiler';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13
@ -193,7 +194,8 @@ describe("A spoiler message", function () {
const hint_input = view.querySelector('.spoiler-hint');
hint_input.value = 'This is the hint';
view.onKeyDown({
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
bottom_panel.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13

View File

@ -11,6 +11,11 @@ import { isArchived } from '@converse/headless/shared/parsers';
import { parseMemberListIQ, parseMUCMessage, parseMUCPresence } from './parsers.js';
import { sendMarker } from '@converse/headless/shared/actions';
const OWNER_COMMANDS = ['owner'];
const ADMIN_COMMANDS = ['admin', 'ban', 'deop', 'destroy', 'member', 'op', 'revoke'];
const MODERATOR_COMMANDS = ['kick', 'mute', 'voice', 'modtools'];
const VISITOR_COMMANDS = ['nick'];
const ACTION_INFO_CODES = ['301', '303', '333', '307', '321', '322'];
const MUCSession = Model.extend({
@ -1213,6 +1218,127 @@ const ChatRoomMixin = {
return api.sendIQ(iq);
},
onCommandError (err) {
const { __ } = _converse;
log.fatal(err);
const message =
__('Sorry, an error happened while running the command.') +
' ' +
__("Check your browser's developer console for details.");
this.createMessage({ message, 'type': 'error' });
},
getNickOrJIDFromCommandArgs (args) {
const { __ } = _converse;
if (u.isValidJID(args.trim())) {
return args.trim();
}
if (!args.startsWith('@')) {
args = '@' + args;
}
const [text, references] = this.parseTextForReferences(args); // eslint-disable-line no-unused-vars
if (!references.length) {
const message = __("Error: couldn't find a groupchat participant based on your arguments");
this.createMessage({ message, 'type': 'error' });
return;
}
if (references.length > 1) {
const message = __('Error: found multiple groupchat participant based on your arguments');
this.createMessage({ message, 'type': 'error' });
return;
}
const nick_or_jid = references.pop().value;
const reason = args.split(nick_or_jid, 2)[1];
if (reason && !reason.startsWith(' ')) {
const message = __("Error: couldn't find a groupchat participant based on your arguments");
this.createMessage({ message, 'type': 'error' });
return;
}
return nick_or_jid;
},
validateRoleOrAffiliationChangeArgs (command, args) {
const { __ } = _converse;
if (!args) {
const message = __(
'Error: the "%1$s" command takes two arguments, the user\'s nickname and optionally a reason.',
command
);
this.createMessage({ message, 'type': 'error' });
return false;
}
return true;
},
getAllowedCommands () {
let allowed_commands = ['clear', 'help', 'me', 'nick', 'register'];
if (this.config.get('changesubject') || ['owner', 'admin'].includes(this.getOwnAffiliation())) {
allowed_commands = [...allowed_commands, ...['subject', 'topic']];
}
const occupant = this.occupants.findWhere({ 'jid': _converse.bare_jid });
if (this.verifyAffiliations(['owner'], occupant, false)) {
allowed_commands = allowed_commands.concat(OWNER_COMMANDS).concat(ADMIN_COMMANDS);
} else if (this.verifyAffiliations(['admin'], occupant, false)) {
allowed_commands = allowed_commands.concat(ADMIN_COMMANDS);
}
if (this.verifyRoles(['moderator'], occupant, false)) {
allowed_commands = allowed_commands.concat(MODERATOR_COMMANDS).concat(VISITOR_COMMANDS);
} else if (!this.verifyRoles(['visitor', 'participant', 'moderator'], occupant, false)) {
allowed_commands = allowed_commands.concat(VISITOR_COMMANDS);
}
allowed_commands.sort();
if (Array.isArray(api.settings.get('muc_disable_slash_commands'))) {
return allowed_commands.filter(c => !api.settings.get('muc_disable_slash_commands').includes(c));
} else {
return allowed_commands;
}
},
verifyAffiliations (affiliations, occupant, show_error = true) {
const { __ } = _converse;
if (!Array.isArray(affiliations)) {
throw new TypeError('affiliations must be an Array');
}
if (!affiliations.length) {
return true;
}
occupant = occupant || this.occupants.findWhere({ 'jid': _converse.bare_jid });
if (occupant) {
const a = occupant.get('affiliation');
if (affiliations.includes(a)) {
return true;
}
}
if (show_error) {
const message = __('Forbidden: you do not have the necessary affiliation in order to do that.');
this.createMessage({ message, 'type': 'error' });
}
return false;
},
verifyRoles (roles, occupant, show_error = true) {
const { __ } = _converse;
if (!Array.isArray(roles)) {
throw new TypeError('roles must be an Array');
}
if (!roles.length) {
return true;
}
occupant = occupant || this.occupants.findWhere({ 'jid': _converse.bare_jid });
if (occupant) {
const role = occupant.get('role');
if (roles.includes(role)) {
return true;
}
}
if (show_error) {
const message = __('Forbidden: you do not have the necessary role in order to do that.');
this.createMessage({ message, 'type': 'error' });
}
return false;
},
/**
* Returns the `role` which the current user has in this MUC
* @private

View File

@ -1,40 +1,51 @@
import debounce from 'lodash/debounce';
import log from '@converse/headless/log';
import tpl_chatbox_message_form from 'templates/chatbox_message_form.js';
import tpl_spinner from 'templates/spinner.js';
import tpl_toolbar from 'templates/toolbar.js';
import { ElementView } from '@converse/skeletor/src/element.js';
import { __ } from 'i18n';
import { _converse, api, converse } from '@converse/headless/core';
import { _converse, api, converse } from "@converse/headless/core";
import { html, render } from 'lit-html';
const u = converse.env.utils;
const { u } = converse.env;
export default class BaseChatView extends ElementView {
export default class ChatBottomPanel extends ElementView {
initDebounced () {
this.markScrolled = debounce(this._markScrolled, 100);
this.debouncedScrollDown = debounce(this.scrollDown, 100);
events = {
'click .send-button': 'onFormSubmitted',
'click .toggle-clear': 'clearMessages',
}
async renderHeading () {
const tpl = await this.generateHeadingTemplate();
render(tpl, this.querySelector('.chat-head-chatbox'));
connectedCallback () {
super.connectedCallback();
this.model = _converse.chatboxes.get(this.getAttribute('jid'));
this.listenTo(this.model, 'change:composing_spoiler', this.renderMessageForm);
this.render();
}
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
render () {
render(html`<div class="message-form-container"></div>`, this);
this.renderMessageForm();
}
renderToolbar () {
if (!api.settings.get('show_toolbar')) {
return this;
}
const options = Object.assign({
'model': this.model,
'chatview': _converse.chatboxviews.get(this.getAttribute('jid'))
},
this.model.toJSON(),
this.getToolbarOptions()
);
render(tpl_toolbar(options), this.querySelector('.chat-toolbar'));
/**
* Triggered once the _converse.ChatBoxView's toolbar has been rendered
* @event _converse#renderToolbar
* @type { _converse.ChatBoxView }
* @example _converse.api.listen.on('renderToolbar', this => { ... });
*/
api.trigger('renderToolbar', this);
return this;
}
renderMessageForm () {
@ -42,6 +53,12 @@ export default class BaseChatView extends ElementView {
render(
tpl_chatbox_message_form(
Object.assign(this.model.toJSON(), {
'onDrop': ev => this.onDrop(ev),
'inputChanged': ev => this.inputChanged(ev),
'onKeyDown': ev => this.onKeyDown(ev),
'onKeyUp': ev => this.onKeyUp(ev),
'onPaste': ev => this.onPaste(ev),
'onChange': ev => this.updateCharCounter(ev.target.value),
'hint_value': this.querySelector('.spoiler-hint')?.value,
'label_message': this.model.get('composing_spoiler') ? __('Hidden message') : __('Message'),
'label_spoiler_hint': __('Optional hint'),
@ -58,88 +75,9 @@ export default class BaseChatView extends ElementView {
this.renderToolbar();
}
renderToolbar () {
if (!api.settings.get('show_toolbar')) {
return this;
}
const options = Object.assign(
{
'model': this.model,
'chatview': this
},
this.model.toJSON(),
this.getToolbarOptions()
);
render(tpl_toolbar(options), this.querySelector('.chat-toolbar'));
/**
* Triggered once the _converse.ChatBoxView's toolbar has been rendered
* @event _converse#renderToolbar
* @type { _converse.ChatBoxView }
* @example _converse.api.listen.on('renderToolbar', view => { ... });
*/
api.trigger('renderToolbar', this);
return this;
}
async getHeadingStandaloneButton (promise_or_data) { // eslint-disable-line class-methods-use-this
const data = await promise_or_data;
return html`
<a
href="#"
class="chatbox-btn ${data.a_class} fa ${data.icon_class}"
@click=${data.handler}
title="${data.i18n_title}"
></a>
`;
}
hideNewMessagesIndicator () {
const new_msgs_indicator = this.querySelector('.new-msgs-indicator');
if (new_msgs_indicator !== null) {
new_msgs_indicator.classList.add('hidden');
}
}
maybeFocus () {
api.settings.get('auto_focus') && this.focus();
}
focus () {
const textarea_el = this.getElementsByClassName('chat-textarea')[0];
if (textarea_el && document.activeElement !== textarea_el) {
textarea_el.focus();
}
return this;
}
show () {
if (this.model.get('hidden')) {
log.debug(`Not showing chat ${this.model.get('jid')} because it's set as hidden`);
return;
}
if (u.isVisible(this)) {
this.maybeFocus();
return;
}
this.afterShown();
}
emitBlurred (ev) {
if (this.contains(document.activeElement) || this.contains(ev.relatedTarget)) {
// Something else in this chatbox is still focused
return;
}
/**
* Triggered when the focus has been removed from a particular chat.
* @event _converse#chatBoxBlurred
* @type { _converse.ChatBoxView | _converse.ChatRoomView }
* @example _converse.api.listen.on('chatBoxBlurred', (view, event) => { ... });
*/
api.trigger('chatBoxBlurred', this, ev);
}
emitFocused (ev) {
if (this.contains(ev.relatedTarget)) {
const chatview = _converse.chatboxviews.get(this.getAttribute('jid'));
if (chatview.contains(document.activeElement) || chatview.contains(ev.relatedTarget)) {
// Something else in this chatbox was already focused
return;
}
@ -152,61 +90,52 @@ export default class BaseChatView extends ElementView {
api.trigger('chatBoxFocused', this, ev);
}
/**
* Scroll to the previously saved scrollTop position, or scroll
* down if it wasn't set.
*/
maintainScrollTop () {
const pos = this.model.get('scrollTop');
if (pos) {
const msgs_container = this.querySelector('.chat-content__messages');
msgs_container.scrollTop = pos;
} else {
this.scrollDown();
emitBlurred (ev) {
const chatview = _converse.chatboxviews.get(this.getAttribute('jid'));
if (!chatview) {
return;
}
}
addSpinner (append = false) {
if (this.querySelector('.spinner') === null) {
const el = u.getElementFromTemplateResult(tpl_spinner());
if (append) {
this.content.insertAdjacentElement('beforeend', el);
this.scrollDown();
} else {
this.content.insertAdjacentElement('afterbegin', el);
}
if (chatview.contains(document.activeElement) || chatview.contains(ev.relatedTarget)) {
// Something else in this chatbox is still focused
return;
}
}
clearSpinner () {
this.content.querySelectorAll('.spinner').forEach(u.removeElement);
}
onStatusMessageChanged (item) {
this.renderHeading();
/**
* When a contact's custom status message has changed.
* @event _converse#contactStatusMessageChanged
* @type {object}
* @property { object } contact - The chat buddy
* @property { string } message - The message text
* @example _converse.api.listen.on('contactStatusMessageChanged', obj => { ... });
* Triggered when the focus has been removed from a particular chat.
* @event _converse#chatBoxBlurred
* @type { _converse.ChatBoxView | _converse.ChatRoomView }
* @example _converse.api.listen.on('chatBoxBlurred', (view, event) => { ... });
*/
api.trigger('contactStatusMessageChanged', {
'contact': item.attributes,
'message': item.get('status')
});
api.trigger('chatBoxBlurred', this, ev);
}
getToolbarOptions () { // eslint-disable-line class-methods-use-this
return {};
}
getOwnMessages () {
return this.model.messages.filter({ 'sender': 'me' });
inputChanged (ev) { // eslint-disable-line class-methods-use-this
const height = ev.target.scrollHeight + 'px';
if (ev.target.style.height != height) {
ev.target.style.height = 'auto';
ev.target.style.height = height;
}
}
onDrop (evt) {
if (evt.dataTransfer.files.length == 0) {
// There are no files to be dropped, so this isnt a file
// transfer operation.
return;
}
evt.preventDefault();
this.model.sendFiles(evt.dataTransfer.files);
}
onDragOver (ev) { // eslint-disable-line class-methods-use-this
ev.preventDefault();
}
async clearMessages (ev) {
if (ev && ev.preventDefault) {
ev.preventDefault();
}
ev?.preventDefault?.();
const result = confirm(__('Are you sure you want to clear the messages from this conversation?'));
if (result === true) {
await this.model.clearMessages();
@ -214,236 +143,23 @@ export default class BaseChatView extends ElementView {
return this;
}
editEarlierMessage () {
let message;
let idx = this.model.messages.findLastIndex('correcting');
if (idx >= 0) {
this.model.messages.at(idx).save('correcting', false);
while (idx > 0) {
idx -= 1;
const candidate = this.model.messages.at(idx);
if (candidate.get('editable')) {
message = candidate;
break;
}
}
}
message =
message ||
this.getOwnMessages()
.reverse()
.find(m => m.get('editable'));
if (message) {
message.save('correcting', true);
}
}
editLaterMessage () {
let message;
let idx = this.model.messages.findLastIndex('correcting');
if (idx >= 0) {
this.model.messages.at(idx).save('correcting', false);
while (idx < this.model.messages.length - 1) {
idx += 1;
const candidate = this.model.messages.at(idx);
if (candidate.get('editable')) {
message = candidate;
break;
}
}
}
if (message) {
this.insertIntoTextArea(u.prefixMentions(message), true, true);
message.save('correcting', true);
} else {
this.insertIntoTextArea('', true, false);
}
}
async getHeadingDropdownItem (promise_or_data) { // eslint-disable-line class-methods-use-this
const data = await promise_or_data;
return html`
<a href="#" class="dropdown-item ${data.a_class}" @click=${data.handler} title="${data.i18n_title}"
><i class="fa ${data.icon_class}"></i>${data.i18n_text}</a
>
`;
}
autocompleteInPicker (input, value) {
const emoji_dropdown = this.querySelector('converse-emoji-dropdown');
const emoji_picker = this.querySelector('converse-emoji-picker');
if (emoji_picker && emoji_dropdown) {
emoji_picker.model.set({
'ac_position': input.selectionStart,
'autocompleting': value,
'query': value
});
emoji_dropdown.showMenu();
return true;
}
}
showNewMessagesIndicator () {
u.showElement(this.querySelector('.new-msgs-indicator'));
}
onMessageAdded (message) {
if (u.isNewMessage(message)) {
if (message.get('sender') === 'me') {
// We remove the "scrolled" flag so that the chat area
// gets scrolled down. We always want to scroll down
// when the user writes a message as opposed to when a
// message is received.
this.model.set('scrolled', false);
} else if (this.model.get('scrolled', true)) {
this.showNewMessagesIndicator();
parseMessageForCommands (text) {
const match = text.replace(/^\s*/, '').match(/^\/(.*)\s*$/);
if (match) {
if (match[1] === 'clear') {
this.clearMessages();
return true;
} else if (match[1] === 'close') {
_converse.chatboxviews.get(this.getAttribute('jid'))?.close();
return true;
} else if (match[1] === 'help') {
this.model.set({ 'show_help_messages': true });
return true;
}
}
}
onEmojiReceivedFromPicker (emoji) {
const model = this.querySelector('converse-emoji-picker').model;
const autocompleting = model.get('autocompleting');
const ac_position = model.get('ac_position');
this.insertIntoTextArea(emoji, autocompleting, false, ac_position);
}
onMessageCorrecting (message) {
if (message.get('correcting')) {
this.insertIntoTextArea(u.prefixMentions(message), true, true);
} else {
const currently_correcting = this.model.messages.findWhere('correcting');
if (currently_correcting && currently_correcting !== message) {
this.insertIntoTextArea(u.prefixMentions(message), true, true);
} else {
this.insertIntoTextArea('', true, false);
}
}
}
/**
* Insert a particular string value into the textarea of this chat box.
* @private
* @method _converse.ChatBoxView#insertIntoTextArea
* @param {string} value - The value to be inserted.
* @param {(boolean|string)} [replace] - Whether an existing value
* should be replaced. If set to `true`, the entire textarea will
* be replaced with the new value. If set to a string, then only
* that string will be replaced *if* a position is also specified.
* @param {integer} [position] - The end index of the string to be
* replaced with the new value.
*/
insertIntoTextArea (value, replace = false, correcting = false, position) {
const textarea = this.querySelector('.chat-textarea');
if (correcting) {
u.addClass('correcting', textarea);
} else {
u.removeClass('correcting', textarea);
}
if (replace) {
if (position && typeof replace == 'string') {
textarea.value = textarea.value.replace(new RegExp(replace, 'g'), (match, offset) =>
offset == position - replace.length ? value + ' ' : match
);
} else {
textarea.value = value;
}
} else {
let existing = textarea.value;
if (existing && existing[existing.length - 1] !== ' ') {
existing = existing + ' ';
}
textarea.value = existing + value + ' ';
}
this.updateCharCounter(textarea.value);
u.placeCaretAtEnd(textarea);
}
/**
* 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 });
}
/**
* Scrolls the chat down.
*
* This method will always scroll the chat down, regardless of
* whether the user scrolled up manually or not.
* @param { Event } [ev] - An optional event that is the cause for needing to scroll down.
*/
scrollDown (ev) {
ev?.preventDefault?.();
ev?.stopPropagation?.();
if (this.model.get('scrolled')) {
u.safeSave(this.model, {
'scrolled': false,
'scrollTop': null
});
}
this.querySelector('.chat-content__messages').scrollDown();
this.onScrolledDown();
}
onScrolledDown () {
this.hideNewMessagesIndicator();
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 }); // TODO: clean up
}
onWindowStateChanged (data) {
if (data.state === 'visible') {
if (!this.model.isHidden() && this.model.get('num_unread', 0)) {
this.model.clearUnreadMsgCounter();
}
} else if (data.state === 'hidden') {
this.model.setChatState(_converse.INACTIVE, { 'silent': true });
this.model.sendChatState();
}
}
async onFormSubmitted (ev) {
ev.preventDefault();
async onFormSubmitted () {
const textarea = this.querySelector('.chat-textarea');
const message_text = textarea.value.trim();
if (
@ -489,7 +205,8 @@ export default class BaseChatView extends ElementView {
if (api.settings.get('view_mode') === 'overlayed') {
// XXX: Chrome flexbug workaround. The .chat-content area
// doesn't resize when the textarea is resized to its original size.
const msgs_container = this.querySelector('.chat-content__messages');
const chatview = _converse.chatboxviews.get(this.getAttribute('jid'));
const msgs_container = chatview.querySelector('.chat-content__messages');
msgs_container.parentElement.style.display = 'none';
}
textarea.removeAttribute('disabled');
@ -497,7 +214,8 @@ export default class BaseChatView extends ElementView {
if (api.settings.get('view_mode') === 'overlayed') {
// XXX: Chrome flexbug workaround.
const msgs_container = this.querySelector('.chat-content__messages');
const chatview = _converse.chatboxviews.get(this.getAttribute('jid'));
const msgs_container = chatview.querySelector('.chat-content__messages');
msgs_container.parentElement.style.display = '';
}
// Suppress events, otherwise superfluous CSN gets set
@ -506,8 +224,162 @@ export default class BaseChatView extends ElementView {
textarea.focus();
}
onEnterPressed (ev) {
return this.onFormSubmitted(ev);
/**
* Insert a particular string value into the textarea of this chat box.
* @param {string} value - The value to be inserted.
* @param {(boolean|string)} [replace] - Whether an existing value
* should be replaced. If set to `true`, the entire textarea will
* be replaced with the new value. If set to a string, then only
* that string will be replaced *if* a position is also specified.
* @param {integer} [position] - The end index of the string to be
* replaced with the new value.
*/
insertIntoTextArea (value, replace = false, correcting = false, position) {
const textarea = this.querySelector('.chat-textarea');
if (correcting) {
u.addClass('correcting', textarea);
} else {
u.removeClass('correcting', textarea);
}
if (replace) {
if (position && typeof replace == 'string') {
textarea.value = textarea.value.replace(new RegExp(replace, 'g'), (match, offset) =>
offset == position - replace.length ? value + ' ' : match
);
} else {
textarea.value = value;
}
} else {
let existing = textarea.value;
if (existing && existing[existing.length - 1] !== ' ') {
existing = existing + ' ';
}
textarea.value = existing + value + ' ';
}
const ev = document.createEvent('HTMLEvents');
ev.initEvent('change', false, true);
textarea.dispatchEvent(ev)
u.placeCaretAtEnd(textarea);
}
onEscapePressed (ev) {
ev.preventDefault();
const idx = this.model.messages.findLastIndex('correcting');
const message = idx >= 0 ? this.model.messages.at(idx) : null;
if (message) {
message.save('correcting', false);
}
this.insertIntoTextArea('', true, false);
}
editEarlierMessage () {
let message;
let idx = this.model.messages.findLastIndex('correcting');
if (idx >= 0) {
this.model.messages.at(idx).save('correcting', false);
while (idx > 0) {
idx -= 1;
const candidate = this.model.messages.at(idx);
if (candidate.get('editable')) {
message = candidate;
break;
}
}
}
message =
message ||
this.model.messages.filter({ 'sender': 'me' })
.reverse()
.find(m => m.get('editable'));
if (message) {
message.save('correcting', true);
}
}
editLaterMessage () {
let message;
let idx = this.model.messages.findLastIndex('correcting');
if (idx >= 0) {
this.model.messages.at(idx).save('correcting', false);
while (idx < this.model.messages.length - 1) {
idx += 1;
const candidate = this.model.messages.at(idx);
if (candidate.get('editable')) {
message = candidate;
break;
}
}
}
if (message) {
this.insertIntoTextArea(u.prefixMentions(message), true, true);
message.save('correcting', true);
} else {
this.insertIntoTextArea('', true, false);
}
}
autocompleteInPicker (input, value) {
const emoji_dropdown = this.querySelector('converse-emoji-dropdown');
const emoji_picker = this.querySelector('converse-emoji-picker');
if (emoji_picker && emoji_dropdown) {
emoji_picker.model.set({
'ac_position': input.selectionStart,
'autocompleting': value,
'query': value
});
emoji_dropdown.showMenu();
return true;
}
}
onKeyDown (ev) {
if (ev.ctrlKey) {
// When ctrl is pressed, no chars are entered into the textarea.
return;
}
if (!ev.shiftKey && !ev.altKey && !ev.metaKey) {
if (ev.keyCode === converse.keycodes.TAB) {
const value = u.getCurrentWord(ev.target, null, /(:.*?:)/g);
if (value.startsWith(':') && this.autocompleteInPicker(ev.target, value)) {
ev.preventDefault();
ev.stopPropagation();
}
} else if (ev.keyCode === converse.keycodes.FORWARD_SLASH) {
// Forward slash is used to run commands. Nothing to do here.
return;
} else if (ev.keyCode === converse.keycodes.ESCAPE) {
return this.onEscapePressed(ev, this);
} else if (ev.keyCode === converse.keycodes.ENTER) {
return this.onFormSubmitted();
} else if (ev.keyCode === converse.keycodes.UP_ARROW && !ev.target.selectionEnd) {
const textarea = this.querySelector('.chat-textarea');
if (!textarea.value || u.hasClass('correcting', textarea)) {
return this.editEarlierMessage();
}
} else if (
ev.keyCode === converse.keycodes.DOWN_ARROW &&
ev.target.selectionEnd === ev.target.value.length &&
u.hasClass('correcting', this.querySelector('.chat-textarea'))
) {
return this.editLaterMessage();
}
}
if (
[
converse.keycodes.SHIFT,
converse.keycodes.META,
converse.keycodes.META_RIGHT,
converse.keycodes.ESCAPE,
converse.keycodes.ALT
].includes(ev.keyCode)
) {
return;
}
if (this.model.get('chat_state') !== _converse.COMPOSING) {
// Set chat state to composing if keyCode is not a forward-slash
// (which would imply an internal command and not a message).
this.model.setChatState(_converse.COMPOSING);
}
}
updateCharCounter (chars) {
@ -522,4 +394,23 @@ export default class BaseChatView extends ElementView {
}
}
}
onKeyUp (ev) {
this.updateCharCounter(ev.target.value);
}
onPaste (ev) {
if (ev.clipboardData.files.length !== 0) {
ev.preventDefault();
// Workaround for quirk in at least Firefox 60.7 ESR:
// It seems that pasted files disappear from the event payload after
// the event has finished, which apparently happens during async
// processing in sendFiles(). So we copy the array here.
this.model.sendFiles(Array.from(ev.clipboardData.files));
return;
}
this.updateCharCounter(ev.clipboardData.getData('text/plain'));
}
}
api.elements.define('converse-chat-bottom-panel', ChatBottomPanel);

View File

@ -0,0 +1,4 @@
<div class="bottom-panel">
<div class="message-form-container"></div>
</div>

View File

@ -1,4 +1,5 @@
import BaseChatView from 'shared/chatview.js';
import 'plugins/chatview/bottom_panel.js';
import BaseChatView from 'shared/chat/baseview.js';
import UserDetailsModal from 'modals/user-details.js';
import tpl_chatbox from 'templates/chatbox.js';
import tpl_chatbox_head from 'templates/chatbox_head.js';
@ -23,12 +24,6 @@ export default class ChatView extends BaseChatView {
events = {
'click .chatbox-navback': 'showControlBox',
'click .new-msgs-indicator': 'viewUnreadMessages',
'click .send-button': 'onFormSubmitted',
'click .toggle-clear': 'clearMessages',
'input .chat-textarea': 'inputChanged',
'keydown .chat-textarea': 'onKeyDown',
'keyup .chat-textarea': 'onKeyUp',
'paste .chat-textarea': 'onPaste'
}
async initialize () {
@ -39,7 +34,6 @@ export default class ChatView extends BaseChatView {
this.initDebounced();
this.listenTo(_converse, 'windowStateChanged', this.onWindowStateChanged);
this.listenTo(this.model, 'change:composing_spoiler', this.renderMessageForm);
this.listenTo(this.model, 'change:hidden', () => !this.model.get('hidden') && this.afterShown());
this.listenTo(this.model, 'change:status', this.onStatusMessageChanged);
this.listenTo(this.model, 'vcard:change', this.renderHeading);
@ -80,7 +74,6 @@ export default class ChatView extends BaseChatView {
render(result, this);
this.content = this.querySelector('.chat-content');
this.help_container = this.querySelector('.chat-content__help');
this.renderMessageForm();
this.renderHeading();
return this;
}
@ -105,20 +98,6 @@ export default class ChatView extends BaseChatView {
api.modal.show(UserDetailsModal, { model: this.model }, ev);
}
onDragOver (evt) { // eslint-disable-line class-methods-use-this
evt.preventDefault();
}
onDrop (evt) {
if (evt.dataTransfer.files.length == 0) {
// There are no files to be dropped, so this isnt a file
// transfer operation.
return;
}
evt.preventDefault();
this.model.sendFiles(evt.dataTransfer.files);
}
async generateHeadingTemplate () {
const vcard = this.model?.vcard;
const vcard_json = vcard ? vcard.toJSON() : {};
@ -195,11 +174,6 @@ export default class ChatView extends BaseChatView {
return _converse.api.hook('getHeadingButtons', this, buttons);
}
getToolbarOptions () { // eslint-disable-line class-methods-use-this
// FIXME: can this be removed?
return {};
}
/**
* Given a message element, determine wether it should be
* marked as a followup message to the previous element.
@ -248,118 +222,6 @@ export default class ChatView extends BaseChatView {
}
}
parseMessageForCommands (text) {
const match = text.replace(/^\s*/, '').match(/^\/(.*)\s*$/);
if (match) {
if (match[1] === 'clear') {
this.clearMessages();
return true;
} else if (match[1] === 'close') {
this.close();
return true;
} else if (match[1] === 'help') {
this.model.set({ 'show_help_messages': true });
return true;
}
}
}
onPaste (ev) {
if (ev.clipboardData.files.length !== 0) {
ev.preventDefault();
// Workaround for quirk in at least Firefox 60.7 ESR:
// It seems that pasted files disappear from the event payload after
// the event has finished, which apparently happens during async
// processing in sendFiles(). So we copy the array here.
this.model.sendFiles(Array.from(ev.clipboardData.files));
return;
}
this.updateCharCounter(ev.clipboardData.getData('text/plain'));
}
/**
* Event handler for when a depressed key goes up
* @private
* @method _converse.ChatBoxView#onKeyUp
*/
onKeyUp (ev) {
this.updateCharCounter(ev.target.value);
}
/**
* Event handler for when a key is pressed down in a chat box textarea.
* @private
* @method _converse.ChatBoxView#onKeyDown
* @param { Event } ev
*/
onKeyDown (ev) {
if (ev.ctrlKey) {
// When ctrl is pressed, no chars are entered into the textarea.
return;
}
if (!ev.shiftKey && !ev.altKey && !ev.metaKey) {
if (ev.keyCode === converse.keycodes.TAB) {
const value = u.getCurrentWord(ev.target, null, /(:.*?:)/g);
if (value.startsWith(':') && this.autocompleteInPicker(ev.target, value)) {
ev.preventDefault();
ev.stopPropagation();
}
} else if (ev.keyCode === converse.keycodes.FORWARD_SLASH) {
// Forward slash is used to run commands. Nothing to do here.
return;
} else if (ev.keyCode === converse.keycodes.ESCAPE) {
return this.onEscapePressed(ev);
} else if (ev.keyCode === converse.keycodes.ENTER) {
return this.onEnterPressed(ev);
} else if (ev.keyCode === converse.keycodes.UP_ARROW && !ev.target.selectionEnd) {
const textarea = this.querySelector('.chat-textarea');
if (!textarea.value || u.hasClass('correcting', textarea)) {
return this.editEarlierMessage();
}
} else if (
ev.keyCode === converse.keycodes.DOWN_ARROW &&
ev.target.selectionEnd === ev.target.value.length &&
u.hasClass('correcting', this.querySelector('.chat-textarea'))
) {
return this.editLaterMessage();
}
}
if (
[
converse.keycodes.SHIFT,
converse.keycodes.META,
converse.keycodes.META_RIGHT,
converse.keycodes.ESCAPE,
converse.keycodes.ALT
].includes(ev.keyCode)
) {
return;
}
if (this.model.get('chat_state') !== _converse.COMPOSING) {
// Set chat state to composing if keyCode is not a forward-slash
// (which would imply an internal command and not a message).
this.model.setChatState(_converse.COMPOSING);
}
}
onEscapePressed (ev) {
ev.preventDefault();
const idx = this.model.messages.findLastIndex('correcting');
const message = idx >= 0 ? this.model.messages.at(idx) : null;
if (message) {
message.save('correcting', false);
}
this.insertIntoTextArea('', true, false);
}
inputChanged (ev) { // eslint-disable-line class-methods-use-this
const height = ev.target.scrollHeight + 'px';
if (ev.target.style.height != height) {
ev.target.style.height = 'auto';
ev.target.style.height = height;
}
}
onPresenceChanged (item) {
const show = item.get('show');
const fullname = this.model.getDisplayName();

View File

@ -1,4 +1,4 @@
import BaseChatView from 'shared/chatview.js';
import BaseChatView from 'shared/chat/baseview.js';
import tpl_chatbox from 'templates/chatbox.js';
import tpl_chat_head from './templates/chat-head.js';
import { __ } from 'i18n';

View File

@ -0,0 +1,310 @@
import BottomPanel from 'plugins/chatview/bottom_panel.js';
import debounce from 'lodash/debounce';
import tpl_muc_bottom_panel from './templates/muc_bottom_panel.js';
import { $pres, Strophe } from 'strophe.js/src/strophe';
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
import { getAutoCompleteListItem } from './utils.js';
import { render } from 'lit-html';
const COMMAND_TO_AFFILIATION = {
'admin': 'admin',
'ban': 'outcast',
'member': 'member',
'owner': 'owner',
'revoke': 'none'
};
const COMMAND_TO_ROLE = {
'deop': 'participant',
'kick': 'none',
'mute': 'visitor',
'op': 'moderator',
'voice': 'participant'
};
const u = converse.env.utils;
export default class MUCBottomPanel extends BottomPanel {
events = {
'click .hide-occupants': 'hideOccupants',
'click .send-button': 'onFormSubmitted',
}
async connectedCallback () {
super.connectedCallback();
this.debouncedRender = debounce(this.render, 100);
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, 'change:hidden_occupants', this.debouncedRender);
this.listenTo(this.model.features, 'change:moderated', this.debouncedRender);
this.listenTo(this.model.occupants, 'add', this.renderIfOwnOccupant)
this.listenTo(this.model.occupants, 'change:role', this.renderIfOwnOccupant);
this.listenTo(this.model.session, 'change:connection_status', this.debouncedRender);
this.render();
}
render () {
const entered = this.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED;
const can_edit = entered && !(this.model.features.get('moderated') && this.model.getOwnRole() === 'visitor');
render(tpl_muc_bottom_panel({ can_edit, entered, 'model': this.model }), this);
if (entered && can_edit) {
this.renderMessageForm();
this.initMentionAutoComplete();
}
}
renderIfOwnOccupant (o) {
(o.get('jid') === _converse.bare_jid) && this.debouncedRender();
}
getToolbarOptions () {
return Object.assign(super.getToolbarOptions(), {
'is_groupchat': true,
'label_hide_occupants': __('Hide the list of participants'),
'show_occupants_toggle': _converse.visible_toolbar_buttons.toggle_occupants
});
}
getAutoCompleteList () {
return this.model.getAllKnownNicknames().map(nick => ({ 'label': nick, 'value': `@${nick}` }));
}
initMentionAutoComplete () {
this.mention_auto_complete = new _converse.AutoComplete(this, {
'auto_first': true,
'auto_evaluate': false,
'min_chars': api.settings.get('muc_mention_autocomplete_min_chars'),
'match_current_word': true,
'list': () => this.getAutoCompleteList(),
'filter':
api.settings.get('muc_mention_autocomplete_filter') == 'contains'
? _converse.FILTER_CONTAINS
: _converse.FILTER_STARTSWITH,
'ac_triggers': ['Tab', '@'],
'include_triggers': [],
'item': getAutoCompleteListItem
});
this.mention_auto_complete.on('suggestion-box-selectcomplete', () => (this.auto_completing = false));
}
/**
* Hide the right sidebar containing the chat occupants.
* @private
* @method _converse.ChatRoomView#hideOccupants
*/
hideOccupants (ev) {
ev?.preventDefault?.();
ev?.stopPropagation?.();
this.model.save({ 'hidden_occupants': true });
_converse.chatboxviews.get(this.getAttribute('jid'))?.scrollDown();
}
onKeyDown (ev) {
if (this.mention_auto_complete.onKeyDown(ev)) {
return;
}
super.onKeyDown(ev);
}
onKeyUp (ev) {
this.mention_auto_complete.evaluate(ev);
super.onKeyUp(ev);
}
setRole (command, args, required_affiliations = [], required_roles = []) {
/* Check that a command to change a groupchat user's role or
* affiliation has anough arguments.
*/
const role = COMMAND_TO_ROLE[command];
if (!role) {
throw Error(`ChatRoomView#setRole called with invalid command: ${command}`);
}
if (!this.model.verifyAffiliations(required_affiliations) || !this.model.verifyRoles(required_roles)) {
return false;
}
if (!this.model.validateRoleOrAffiliationChangeArgs(command, args)) {
return false;
}
const nick_or_jid = this.model.getNickOrJIDFromCommandArgs(args);
if (!nick_or_jid) {
return false;
}
const reason = args.split(nick_or_jid, 2)[1].trim();
// We're guaranteed to have an occupant due to getNickOrJIDFromCommandArgs
const occupant = this.model.getOccupant(nick_or_jid);
this.model.setRole(occupant, role, reason, undefined, this.model.onCommandError.bind(this));
return true;
}
setAffiliation (command, args, required_affiliations) {
const affiliation = COMMAND_TO_AFFILIATION[command];
if (!affiliation) {
throw Error(`ChatRoomView#setAffiliation called with invalid command: ${command}`);
}
if (!this.model.verifyAffiliations(required_affiliations)) {
return false;
}
if (!this.model.validateRoleOrAffiliationChangeArgs(command, args)) {
return false;
}
const nick_or_jid = this.model.getNickOrJIDFromCommandArgs(args);
if (!nick_or_jid) {
return false;
}
let jid;
const reason = args.split(nick_or_jid, 2)[1].trim();
const occupant = this.model.getOccupant(nick_or_jid);
if (occupant) {
jid = occupant.get('jid');
} else {
if (u.isValidJID(nick_or_jid)) {
jid = nick_or_jid;
} else {
const message = __(
"Couldn't find a participant with that nickname. " + 'They might have left the groupchat.'
);
this.model.createMessage({ message, 'type': 'error' });
return;
}
}
const attrs = { jid, reason };
if (occupant && api.settings.get('auto_register_muc_nickname')) {
attrs['nick'] = occupant.get('nick');
}
this.model
.setAffiliation(affiliation, [attrs])
.then(() => this.model.occupants.fetchMembers())
.catch(err => this.model.onCommandError(err));
}
parseMessageForCommands (text) {
if (
api.settings.get('muc_disable_slash_commands') &&
!Array.isArray(api.settings.get('muc_disable_slash_commands'))
) {
return super.parseMessageForCommands(text);
}
text = text.replace(/^\s*/, '');
const command = (text.match(/^\/([a-zA-Z]*) ?/) || ['']).pop().toLowerCase();
if (!command) {
return false;
}
const args = text.slice(('/' + command).length + 1).trim();
if (!this.model.getAllowedCommands().includes(command)) {
return false;
}
switch (command) {
case 'admin': {
this.setAffiliation(command, args, ['owner']);
break;
}
case 'ban': {
this.setAffiliation(command, args, ['admin', 'owner']);
break;
}
case 'modtools': {
const chatview = _converse.chatboxviews.get(this.getAttribute('jid'));
chatview.showModeratorToolsModal(args);
break;
}
case 'deop': {
// FIXME: /deop only applies to setting a moderators
// role to "participant" (which only admin/owner can
// do). Moderators can however set non-moderator's role
// to participant (e.g. visitor => participant).
// Currently we don't distinguish between these two
// cases.
this.setRole(command, args, ['admin', 'owner']);
break;
}
case 'destroy': {
if (!this.model.verifyAffiliations(['owner'])) {
break;
}
const chatview = _converse.chatboxviews.get(this.getAttribute('jid'));
chatview.destroy().catch(e => this.model.onCommandError(e));
break;
}
case 'help': {
this.model.set({ 'show_help_messages': true });
break;
}
case 'kick': {
this.setRole(command, args, [], ['moderator']);
break;
}
case 'mute': {
this.setRole(command, args, [], ['moderator']);
break;
}
case 'member': {
this.setAffiliation(command, args, ['admin', 'owner']);
break;
}
case 'nick': {
if (!this.model.verifyRoles(['visitor', 'participant', 'moderator'])) {
break;
} else if (args.length === 0) {
// e.g. Your nickname is "coolguy69"
const message = __('Your nickname is "%1$s"', this.model.get('nick'));
this.model.createMessage({ message, 'type': 'error' });
} else {
const jid = Strophe.getBareJidFromJid(this.model.get('jid'));
api.send(
$pres({
from: _converse.connection.jid,
to: `${jid}/${args}`,
id: u.getUniqueId()
}).tree()
);
}
break;
}
case 'owner':
this.setAffiliation(command, args, ['owner']);
break;
case 'op': {
this.setRole(command, args, ['admin', 'owner']);
break;
}
case 'register': {
if (args.length > 1) {
this.model.createMessage({
'message': __('Error: invalid number of arguments'),
'type': 'error'
});
} else {
this.model.registerNickname().then(err_msg => {
err_msg && this.model.createMessage({ 'message': err_msg, 'type': 'error' });
});
}
break;
}
case 'revoke': {
this.setAffiliation(command, args, ['admin', 'owner']);
break;
}
case 'topic':
case 'subject':
this.model.setSubject(args);
break;
case 'voice': {
this.setRole(command, args, [], ['moderator']);
break;
}
default:
return super.parseMessageForCommands(text);
}
return true;
}
}
api.elements.define('converse-muc-bottom-panel', MUCBottomPanel);

View File

@ -1,19 +1,18 @@
import './bottom_panel.js';
import './config-form.js';
import './password-form.js';
import 'shared/autocomplete/index.js';
import BaseChatView from 'shared/chatview.js';
import BaseChatView from 'shared/chat/baseview.js';
import MUCInviteModal from 'modals/muc-invite.js';
import ModeratorToolsModal from 'modals/moderator-tools.js';
import RoomDetailsModal from 'modals/muc-details.js';
import log from '@converse/headless/log';
import tpl_muc from './templates/muc.js';
import tpl_muc_head from './templates/muc_head.js';
import tpl_muc_bottom_panel from './templates/muc_bottom_panel.js';
import tpl_muc_destroyed from './templates/muc_destroyed.js';
import tpl_muc_disconnect from './templates/muc_disconnect.js';
import tpl_muc_nickname_form from './templates/muc_nickname_form.js';
import tpl_spinner from 'templates/spinner.js';
import { $pres, Strophe } from 'strophe.js/src/strophe';
import { Model } from '@converse/skeletor/src/model.js';
import { __ } from 'i18n';
import { _converse, api, converse } from '@converse/headless/core';
@ -23,26 +22,6 @@ import { render } from 'lit-html';
const { sizzle } = converse.env;
const u = converse.env.utils;
const OWNER_COMMANDS = ['owner'];
const ADMIN_COMMANDS = ['admin', 'ban', 'deop', 'destroy', 'member', 'op', 'revoke'];
const MODERATOR_COMMANDS = ['kick', 'mute', 'voice', 'modtools'];
const VISITOR_COMMANDS = ['nick'];
const COMMAND_TO_ROLE = {
'deop': 'participant',
'kick': 'none',
'mute': 'visitor',
'op': 'moderator',
'voice': 'participant'
};
const COMMAND_TO_AFFILIATION = {
'admin': 'admin',
'ban': 'outcast',
'member': 'member',
'owner': 'owner',
'revoke': 'none'
};
/**
* Mixin which turns a ChatBoxView into a ChatRoomView
* @mixin
@ -62,14 +41,7 @@ export default class MUCView extends BaseChatView {
'click .occupant-nick': function (ev) {
this.insertIntoTextArea(ev.target.textContent);
},
'click .send-button': 'onFormSubmitted',
'dragover .chat-textarea': 'onDragOver',
'drop .chat-textarea': 'onDrop',
'input .chat-textarea': 'inputChanged',
'keydown .chat-textarea': 'onKeyDown',
'keyup .chat-textarea': 'onKeyUp',
'mousedown .dragresize-occupants-left': 'onStartResizeOccupants',
'paste .chat-textarea': 'onPaste',
'submit .muc-nickname-form': 'submitNickname'
}
@ -88,7 +60,6 @@ export default class MUCView extends BaseChatView {
this.listenTo(this.model, 'change:minimized', () => this.afterShown());
this.listenTo(this.model, 'configurationNeeded', this.getAndRenderConfigurationForm);
this.listenTo(this.model, 'show', this.show);
this.listenTo(this.model.features, 'change:moderated', this.renderBottomPanel);
this.listenTo(this.model.features, 'change:open', this.renderHeading);
this.listenTo(this.model.messages, 'change:correcting', this.onMessageCorrecting);
this.listenTo(this.model.session, 'change:connection_status', this.onConnectionStatusChanged);
@ -106,7 +77,6 @@ export default class MUCView extends BaseChatView {
this.model.occupants.forEach(o => this.onOccupantAdded(o));
this.listenTo(this.model.occupants, 'add', this.onOccupantAdded);
this.listenTo(this.model.occupants, 'change:affiliation', this.onOccupantAffiliationChanged);
this.listenTo(this.model.occupants, 'change:role', this.onOccupantRoleChanged);
this.listenTo(this.model.occupants, 'change:show', this.showJoinOrLeaveNotification);
this.listenTo(this.model.occupants, 'remove', this.onOccupantRemoved);
@ -147,7 +117,6 @@ export default class MUCView extends BaseChatView {
this.content = this.querySelector('.chat-content');
this.help_container = this.querySelector('.chat-content__help');
this.renderBottomPanel();
if (
!api.settings.get('muc_show_logs_before_join') &&
this.model.session.get('connection_status') !== converse.ROOMSTATUS.ENTERED
@ -187,7 +156,7 @@ export default class MUCView extends BaseChatView {
`<strong>/voice</strong>: ${__('Allow muted user to post messages')}`
]
.filter(line => disabled_commands.every(c => !line.startsWith(c + '<', 9)))
.filter(line => this.getAllowedCommands().some(c => line.startsWith(c + '<', 9)));
.filter(line => this.model.getAllowedCommands().some(c => line.startsWith(c + '<', 9)));
}
/**
@ -201,17 +170,6 @@ export default class MUCView extends BaseChatView {
render(tpl, this.querySelector('.chat-head-chatroom'));
}
renderBottomPanel () {
const container = this.querySelector('.bottom-panel');
const entered = this.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED;
const can_edit = entered && !(this.model.features.get('moderated') && this.model.getOwnRole() === 'visitor');
render(tpl_muc_bottom_panel({ can_edit, entered }), container);
if (entered && can_edit) {
this.renderMessageForm();
this.initMentionAutoComplete();
}
}
onStartResizeOccupants (ev) {
this.resizing = true;
this.addEventListener('mousemove', this.onMouseMove);
@ -282,64 +240,6 @@ export default class MUCView extends BaseChatView {
return occupants_width;
}
getAutoCompleteList () {
return this.model.getAllKnownNicknames().map(nick => ({ 'label': nick, 'value': `@${nick}` }));
}
getAutoCompleteListItem (text, input) { // eslint-disable-line class-methods-use-this
input = input.trim();
const element = document.createElement('li');
element.setAttribute('aria-selected', 'false');
if (api.settings.get('muc_mention_autocomplete_show_avatar')) {
const img = document.createElement('img');
let dataUri = 'data:' + _converse.DEFAULT_IMAGE_TYPE + ';base64,' + _converse.DEFAULT_IMAGE;
if (_converse.vcards) {
const vcard = _converse.vcards.findWhere({ 'nickname': text });
if (vcard) dataUri = 'data:' + vcard.get('image_type') + ';base64,' + vcard.get('image');
}
img.setAttribute('src', dataUri);
img.setAttribute('width', '22');
img.setAttribute('class', 'avatar avatar-autocomplete');
element.appendChild(img);
}
const regex = new RegExp('(' + input + ')', 'ig');
const parts = input ? text.split(regex) : [text];
parts.forEach(txt => {
if (input && txt.match(regex)) {
const match = document.createElement('mark');
match.textContent = txt;
element.appendChild(match);
} else {
element.appendChild(document.createTextNode(txt));
}
});
return element;
}
initMentionAutoComplete () {
this.mention_auto_complete = new _converse.AutoComplete(this, {
'auto_first': true,
'auto_evaluate': false,
'min_chars': api.settings.get('muc_mention_autocomplete_min_chars'),
'match_current_word': true,
'list': () => this.getAutoCompleteList(),
'filter':
api.settings.get('muc_mention_autocomplete_filter') == 'contains'
? _converse.FILTER_CONTAINS
: _converse.FILTER_STARTSWITH,
'ac_triggers': ['Tab', '@'],
'include_triggers': [],
'item': this.getAutoCompleteListItem
});
this.mention_auto_complete.on('suggestion-box-selectcomplete', () => (this.auto_completing = false));
}
/**
* Get the nickname value from the form and then join the groupchat with it.
* @private
@ -352,20 +252,8 @@ export default class MUCView extends BaseChatView {
nick && this.model.join(nick);
}
onKeyDown (ev) {
if (this.mention_auto_complete.onKeyDown(ev)) {
return;
}
return _converse.ChatBoxView.prototype.onKeyDown.call(this, ev);
}
onKeyUp (ev) {
this.mention_auto_complete.evaluate(ev);
return _converse.ChatBoxView.prototype.onKeyUp.call(this, ev);
}
showModeratorToolsModal (affiliation) {
if (!this.verifyRoles(['moderator'])) {
if (!this.model.verifyRoles(['moderator'])) {
return;
}
let modal = api.modal.get(ModeratorToolsModal.id);
@ -408,12 +296,6 @@ export default class MUCView extends BaseChatView {
}
}
onOccupantRoleChanged (occupant) {
if (occupant.get('jid') === _converse.bare_jid) {
this.renderBottomPanel();
}
}
/**
* Returns a list of objects which represent buttons for the groupchat header.
* @emits _converse#getHeadingButtons
@ -469,7 +351,7 @@ export default class MUCView extends BaseChatView {
const conn_status = this.model.session.get('connection_status');
if (conn_status === converse.ROOMSTATUS.ENTERED) {
const allowed_commands = this.getAllowedCommands();
const allowed_commands = this.model.getAllowedCommands();
if (allowed_commands.includes('modtools')) {
buttons.push({
'i18n_text': __('Moderate'),
@ -562,7 +444,6 @@ export default class MUCView extends BaseChatView {
} else if (conn_status === converse.ROOMSTATUS.CONNECTING) {
this.showSpinner();
} else if (conn_status === converse.ROOMSTATUS.ENTERED) {
this.renderBottomPanel();
this.hideSpinner();
this.maybeFocus();
} else if (conn_status === converse.ROOMSTATUS.DISCONNECTED) {
@ -572,14 +453,6 @@ export default class MUCView extends BaseChatView {
}
}
getToolbarOptions () {
return Object.assign(_converse.ChatBoxView.prototype.getToolbarOptions.apply(this, arguments), {
'is_groupchat': true,
'label_hide_occupants': __('Hide the list of participants'),
'show_occupants_toggle': _converse.visible_toolbar_buttons.toggle_occupants
});
}
/**
* Closes this chat box, which implies leaving the groupchat as well.
* @private
@ -606,193 +479,10 @@ export default class MUCView extends BaseChatView {
this.scrollDown();
}
verifyRoles (roles, occupant, show_error = true) {
if (!Array.isArray(roles)) {
throw new TypeError('roles must be an Array');
}
if (!roles.length) {
return true;
}
occupant = occupant || this.model.occupants.findWhere({ 'jid': _converse.bare_jid });
if (occupant) {
const role = occupant.get('role');
if (roles.includes(role)) {
return true;
}
}
if (show_error) {
const message = __('Forbidden: you do not have the necessary role in order to do that.');
this.model.createMessage({ message, 'type': 'error' });
}
return false;
}
verifyAffiliations (affiliations, occupant, show_error = true) {
if (!Array.isArray(affiliations)) {
throw new TypeError('affiliations must be an Array');
}
if (!affiliations.length) {
return true;
}
occupant = occupant || this.model.occupants.findWhere({ 'jid': _converse.bare_jid });
if (occupant) {
const a = occupant.get('affiliation');
if (affiliations.includes(a)) {
return true;
}
}
if (show_error) {
const message = __('Forbidden: you do not have the necessary affiliation in order to do that.');
this.model.createMessage({ message, 'type': 'error' });
}
return false;
}
validateRoleOrAffiliationChangeArgs (command, args) {
if (!args) {
const message = __(
'Error: the "%1$s" command takes two arguments, the user\'s nickname and optionally a reason.',
command
);
this.model.createMessage({ message, 'type': 'error' });
return false;
}
return true;
}
getNickOrJIDFromCommandArgs (args) {
if (u.isValidJID(args.trim())) {
return args.trim();
}
if (!args.startsWith('@')) {
args = '@' + args;
}
const [text, references] = this.model.parseTextForReferences(args); // eslint-disable-line no-unused-vars
if (!references.length) {
const message = __("Error: couldn't find a groupchat participant based on your arguments");
this.model.createMessage({ message, 'type': 'error' });
return;
}
if (references.length > 1) {
const message = __('Error: found multiple groupchat participant based on your arguments');
this.model.createMessage({ message, 'type': 'error' });
return;
}
const nick_or_jid = references.pop().value;
const reason = args.split(nick_or_jid, 2)[1];
if (reason && !reason.startsWith(' ')) {
const message = __("Error: couldn't find a groupchat participant based on your arguments");
this.model.createMessage({ message, 'type': 'error' });
return;
}
return nick_or_jid;
}
setAffiliation (command, args, required_affiliations) {
const affiliation = COMMAND_TO_AFFILIATION[command];
if (!affiliation) {
throw Error(`ChatRoomView#setAffiliation called with invalid command: ${command}`);
}
if (!this.verifyAffiliations(required_affiliations)) {
return false;
}
if (!this.validateRoleOrAffiliationChangeArgs(command, args)) {
return false;
}
const nick_or_jid = this.getNickOrJIDFromCommandArgs(args);
if (!nick_or_jid) {
return false;
}
let jid;
const reason = args.split(nick_or_jid, 2)[1].trim();
const occupant = this.model.getOccupant(nick_or_jid);
if (occupant) {
jid = occupant.get('jid');
} else {
if (u.isValidJID(nick_or_jid)) {
jid = nick_or_jid;
} else {
const message = __(
"Couldn't find a participant with that nickname. " + 'They might have left the groupchat.'
);
this.model.createMessage({ message, 'type': 'error' });
return;
}
}
const attrs = { jid, reason };
if (occupant && api.settings.get('auto_register_muc_nickname')) {
attrs['nick'] = occupant.get('nick');
}
this.model
.setAffiliation(affiliation, [attrs])
.then(() => this.model.occupants.fetchMembers())
.catch(err => this.onCommandError(err));
}
getReason (args) { // eslint-disable-line class-methods-use-this
return args.includes(',') ? args.slice(args.indexOf(',') + 1).trim() : null;
}
setRole (command, args, required_affiliations = [], required_roles = []) {
/* Check that a command to change a groupchat user's role or
* affiliation has anough arguments.
*/
const role = COMMAND_TO_ROLE[command];
if (!role) {
throw Error(`ChatRoomView#setRole called with invalid command: ${command}`);
}
if (!this.verifyAffiliations(required_affiliations) || !this.verifyRoles(required_roles)) {
return false;
}
if (!this.validateRoleOrAffiliationChangeArgs(command, args)) {
return false;
}
const nick_or_jid = this.getNickOrJIDFromCommandArgs(args);
if (!nick_or_jid) {
return false;
}
const reason = args.split(nick_or_jid, 2)[1].trim();
// We're guaranteed to have an occupant due to getNickOrJIDFromCommandArgs
const occupant = this.model.getOccupant(nick_or_jid);
this.model.setRole(occupant, role, reason, undefined, this.onCommandError.bind(this));
return true;
}
onCommandError (err) {
log.fatal(err);
const message =
__('Sorry, an error happened while running the command.') +
' ' +
__("Check your browser's developer console for details.");
this.model.createMessage({ message, 'type': 'error' });
}
getAllowedCommands () {
let allowed_commands = ['clear', 'help', 'me', 'nick', 'register'];
if (this.model.config.get('changesubject') || ['owner', 'admin'].includes(this.model.getOwnAffiliation())) {
allowed_commands = [...allowed_commands, ...['subject', 'topic']];
}
const occupant = this.model.occupants.findWhere({ 'jid': _converse.bare_jid });
if (this.verifyAffiliations(['owner'], occupant, false)) {
allowed_commands = allowed_commands.concat(OWNER_COMMANDS).concat(ADMIN_COMMANDS);
} else if (this.verifyAffiliations(['admin'], occupant, false)) {
allowed_commands = allowed_commands.concat(ADMIN_COMMANDS);
}
if (this.verifyRoles(['moderator'], occupant, false)) {
allowed_commands = allowed_commands.concat(MODERATOR_COMMANDS).concat(VISITOR_COMMANDS);
} else if (!this.verifyRoles(['visitor', 'participant', 'moderator'], occupant, false)) {
allowed_commands = allowed_commands.concat(VISITOR_COMMANDS);
}
allowed_commands.sort();
if (Array.isArray(api.settings.get('muc_disable_slash_commands'))) {
return allowed_commands.filter(c => !api.settings.get('muc_disable_slash_commands').includes(c));
} else {
return allowed_commands;
}
}
async destroy () {
const messages = [__('Are you sure you want to destroy this groupchat?')];
let fields = [
@ -824,126 +514,6 @@ export default class MUCView extends BaseChatView {
}
}
parseMessageForCommands (text) {
if (
api.settings.get('muc_disable_slash_commands') &&
!Array.isArray(api.settings.get('muc_disable_slash_commands'))
) {
return _converse.ChatBoxView.prototype.parseMessageForCommands.apply(this, arguments);
}
text = text.replace(/^\s*/, '');
const command = (text.match(/^\/([a-zA-Z]*) ?/) || ['']).pop().toLowerCase();
if (!command) {
return false;
}
const args = text.slice(('/' + command).length + 1).trim();
if (!this.getAllowedCommands().includes(command)) {
return false;
}
switch (command) {
case 'admin': {
this.setAffiliation(command, args, ['owner']);
break;
}
case 'ban': {
this.setAffiliation(command, args, ['admin', 'owner']);
break;
}
case 'modtools': {
this.showModeratorToolsModal(args);
break;
}
case 'deop': {
// FIXME: /deop only applies to setting a moderators
// role to "participant" (which only admin/owner can
// do). Moderators can however set non-moderator's role
// to participant (e.g. visitor => participant).
// Currently we don't distinguish between these two
// cases.
this.setRole(command, args, ['admin', 'owner']);
break;
}
case 'destroy': {
if (!this.verifyAffiliations(['owner'])) {
break;
}
this.destroy().catch(e => this.onCommandError(e));
break;
}
case 'help': {
this.model.set({ 'show_help_messages': true });
break;
}
case 'kick': {
this.setRole(command, args, [], ['moderator']);
break;
}
case 'mute': {
this.setRole(command, args, [], ['moderator']);
break;
}
case 'member': {
this.setAffiliation(command, args, ['admin', 'owner']);
break;
}
case 'nick': {
if (!this.verifyRoles(['visitor', 'participant', 'moderator'])) {
break;
} else if (args.length === 0) {
// e.g. Your nickname is "coolguy69"
const message = __('Your nickname is "%1$s"', this.model.get('nick'));
this.model.createMessage({ message, 'type': 'error' });
} else {
const jid = Strophe.getBareJidFromJid(this.model.get('jid'));
api.send(
$pres({
from: _converse.connection.jid,
to: `${jid}/${args}`,
id: u.getUniqueId()
}).tree()
);
}
break;
}
case 'owner':
this.setAffiliation(command, args, ['owner']);
break;
case 'op': {
this.setRole(command, args, ['admin', 'owner']);
break;
}
case 'register': {
if (args.length > 1) {
this.model.createMessage({
'message': __('Error: invalid number of arguments'),
'type': 'error'
});
} else {
this.model.registerNickname().then(err_msg => {
err_msg && this.model.createMessage({ 'message': err_msg, 'type': 'error' });
});
}
break;
}
case 'revoke': {
this.setAffiliation(command, args, ['admin', 'owner']);
break;
}
case 'topic':
case 'subject':
this.model.setSubject(args);
break;
case 'voice': {
this.setRole(command, args, [], ['moderator']);
break;
}
default:
return _converse.ChatBoxView.prototype.parseMessageForCommands.apply(this, arguments);
}
return true;
}
/**
* Renders a form given an IQ stanza containing the current
* groupchat configuration.
@ -973,15 +543,12 @@ export default class MUCView extends BaseChatView {
* @method _converse.ChatRoomView#renderNicknameForm
*/
renderNicknameForm () {
const tpl_result = tpl_muc_nickname_form(this.model.toJSON());
if (api.settings.get('muc_show_logs_before_join')) {
this.hideSpinner();
u.showElement(this.querySelector('.chat-area'));
const container = this.querySelector('.muc-bottom-panel');
render(tpl_result, container);
u.addClass('muc-bottom-panel--nickname', container);
} else {
const form = this.querySelector('.muc-nickname-form');
const tpl_result = tpl_muc_nickname_form(this.model.toJSON());
const form_el = u.getElementFromTemplateResult(tpl_result);
if (form) {
sizzle('.spinner', this).forEach(u.removeElement);
@ -1115,7 +682,6 @@ export default class MUCView extends BaseChatView {
onOccupantAdded (occupant) {
if (occupant.get('jid') === _converse.bare_jid) {
this.renderHeading();
this.renderBottomPanel();
}
}

View File

@ -14,12 +14,13 @@ export default (o) => html`
<div class="chat-content__help"></div>
</div>
<div class="bottom-panel"></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>
</div>
</div>
`;

View File

@ -1,4 +1,6 @@
import tpl_muc_nickname_form from './muc_nickname_form.js';
import { __ } from 'i18n';
import { api, converse } from "@converse/headless/core";
import { html } from "lit-html";
@ -8,10 +10,15 @@ const tpl_can_edit = () => html`
export default (o) => {
const conn_status = o.model.session.get('connection_status');
const i18n_not_allowed = __("You're not allowed to send messages in this room");
if (o.entered) {
return (o.can_edit) ? tpl_can_edit() : html`<div class="muc-bottom-panel">${i18n_not_allowed}</div>`;
if (conn_status === converse.ROOMSTATUS.ENTERED) {
return (o.can_edit) ? tpl_can_edit() : html`<span class="muc-bottom-panel muc-bottom-panel--muted">${i18n_not_allowed}</span>`;
} else if (conn_status == converse.ROOMSTATUS.NICKNAME_REQUIRED) {
if (api.settings.get('muc_show_logs_before_join')) {
return html`<span class="muc-bottom-panel muc-bottom-panel--nickname">${tpl_muc_nickname_form(o.model.toJSON())}</span>`;
}
} else {
return html`<div class="muc-bottom-panel"></div>`;
return '';
}
}

View File

@ -0,0 +1,38 @@
import { _converse, api } from "@converse/headless/core";
export function getAutoCompleteListItem (text, input) {
input = input.trim();
const element = document.createElement('li');
element.setAttribute('aria-selected', 'false');
if (api.settings.get('muc_mention_autocomplete_show_avatar')) {
const img = document.createElement('img');
let dataUri = 'data:' + _converse.DEFAULT_IMAGE_TYPE + ';base64,' + _converse.DEFAULT_IMAGE;
if (_converse.vcards) {
const vcard = _converse.vcards.findWhere({ 'nickname': text });
if (vcard) dataUri = 'data:' + vcard.get('image_type') + ';base64,' + vcard.get('image');
}
img.setAttribute('src', dataUri);
img.setAttribute('width', '22');
img.setAttribute('class', 'avatar avatar-autocomplete');
element.appendChild(img);
}
const regex = new RegExp('(' + input + ')', 'ig');
const parts = input ? text.split(regex) : [text];
parts.forEach(txt => {
if (input && txt.match(regex)) {
const match = document.createElement('mark');
match.textContent = txt;
element.appendChild(match);
} else {
element.appendChild(document.createTextNode(txt));
}
});
return element;
}

278
src/shared/chat/baseview.js Normal file
View File

@ -0,0 +1,278 @@
import debounce from 'lodash/debounce';
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;
export default class BaseChatView extends ElementView {
initDebounced () {
this.markScrolled = debounce(this._markScrolled, 100);
this.debouncedScrollDown = debounce(this.scrollDown, 100);
}
async renderHeading () {
const tpl = await this.generateHeadingTemplate();
render(tpl, this.querySelector('.chat-head-chatbox'));
}
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
);
}
async getHeadingStandaloneButton (promise_or_data) { // eslint-disable-line class-methods-use-this
const data = await promise_or_data;
return html`
<a
href="#"
class="chatbox-btn ${data.a_class} fa ${data.icon_class}"
@click=${data.handler}
title="${data.i18n_title}"
></a>
`;
}
hideNewMessagesIndicator () {
const new_msgs_indicator = this.querySelector('.new-msgs-indicator');
if (new_msgs_indicator !== null) {
new_msgs_indicator.classList.add('hidden');
}
}
maybeFocus () {
api.settings.get('auto_focus') && this.focus();
}
focus () {
const textarea_el = this.getElementsByClassName('chat-textarea')[0];
if (textarea_el && document.activeElement !== textarea_el) {
textarea_el.focus();
}
return this;
}
show () {
if (this.model.get('hidden')) {
log.debug(`Not showing chat ${this.model.get('jid')} because it's set as hidden`);
return;
}
if (u.isVisible(this)) {
this.maybeFocus();
return;
}
this.afterShown();
}
/**
* Scroll to the previously saved scrollTop position, or scroll
* down if it wasn't set.
*/
maintainScrollTop () {
const pos = this.model.get('scrollTop');
if (pos) {
const msgs_container = this.querySelector('.chat-content__messages');
msgs_container.scrollTop = pos;
} else {
this.scrollDown();
}
}
addSpinner (append = false) {
if (this.querySelector('.spinner') === null) {
const el = u.getElementFromTemplateResult(tpl_spinner());
if (append) {
this.content.insertAdjacentElement('beforeend', el);
this.scrollDown();
} else {
this.content.insertAdjacentElement('afterbegin', el);
}
}
}
clearSpinner () {
this.content.querySelectorAll('.spinner').forEach(u.removeElement);
}
onStatusMessageChanged (item) {
this.renderHeading();
/**
* When a contact's custom status message has changed.
* @event _converse#contactStatusMessageChanged
* @type {object}
* @property { object } contact - The chat buddy
* @property { string } message - The message text
* @example _converse.api.listen.on('contactStatusMessageChanged', obj => { ... });
*/
api.trigger('contactStatusMessageChanged', {
'contact': item.attributes,
'message': item.get('status')
});
}
async getHeadingDropdownItem (promise_or_data) { // eslint-disable-line class-methods-use-this
const data = await promise_or_data;
return html`
<a href="#" class="dropdown-item ${data.a_class}" @click=${data.handler} title="${data.i18n_title}"
><i class="fa ${data.icon_class}"></i>${data.i18n_text}</a
>
`;
}
showNewMessagesIndicator () {
u.showElement(this.querySelector('.new-msgs-indicator'));
}
onMessageAdded (message) {
if (u.isNewMessage(message)) {
if (message.get('sender') === 'me') {
// We remove the "scrolled" flag so that the chat area
// gets scrolled down. We always want to scroll down
// when the user writes a message as opposed to when a
// message is received.
this.model.set('scrolled', false);
} else if (this.model.get('scrolled', true)) {
this.showNewMessagesIndicator();
}
}
}
onEmojiReceivedFromPicker (emoji) {
const model = this.querySelector('converse-emoji-picker').model;
const autocompleting = model.get('autocompleting');
const ac_position = model.get('ac_position');
this.insertIntoTextArea(emoji, autocompleting, false, ac_position);
}
onMessageCorrecting (message) {
if (message.get('correcting')) {
this.insertIntoTextArea(u.prefixMentions(message), true, true);
} else {
const currently_correcting = this.model.messages.findWhere('correcting');
if (currently_correcting && currently_correcting !== message) {
this.insertIntoTextArea(u.prefixMentions(message), true, true);
} else {
this.insertIntoTextArea('', true, false);
}
}
}
/**
* Insert a particular string value into the textarea of this chat box.
* @private
* @method _converse.ChatBoxView#insertIntoTextArea
* @param {string} value - The value to be inserted.
* @param {(boolean|string)} [replace] - Whether an existing value
* should be replaced. If set to `true`, the entire textarea will
* be replaced with the new value. If set to a string, then only
* that string will be replaced *if* a position is also specified.
* @param {integer} [position] - The end index of the string to be
* replaced with the new value.
*/
insertIntoTextArea (value, replace = false, correcting = false, position) {
let bottom_panel;
if (this.model.get('type') === _converse.CHATROOMS_TYPE) {
bottom_panel = this.querySelector('converse-muc-bottom-panel');
} else {
bottom_panel = this.querySelector('converse-chat-bottom-panel');
}
bottom_panel.insertIntoTextArea(value, replace, correcting, position);
}
/**
* 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 });
}
/**
* Scrolls the chat down.
*
* This method will always scroll the chat down, regardless of
* whether the user scrolled up manually or not.
* @param { Event } [ev] - An optional event that is the cause for needing to scroll down.
*/
scrollDown (ev) {
ev?.preventDefault?.();
ev?.stopPropagation?.();
if (this.model.get('scrolled')) {
u.safeSave(this.model, {
'scrolled': false,
'scrollTop': null
});
}
this.querySelector('.chat-content__messages').scrollDown();
this.onScrolledDown();
}
onScrolledDown () {
this.hideNewMessagesIndicator();
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 }); // TODO: clean up
}
onWindowStateChanged (data) {
if (data.state === 'visible') {
if (!this.model.isHidden() && this.model.get('num_unread', 0)) {
this.model.clearUnreadMsgCounter();
}
} else if (data.state === 'hidden') {
this.model.setChatState(_converse.INACTIVE, { 'silent': true });
this.model.sendChatState();
}
}
}

View File

@ -13,9 +13,7 @@ export default (o) => html`
<div class="chat-content__help"></div>
</div>
<div class="bottom-panel">
<div class="message-form-container">
</div>
<converse-chat-bottom-panel jid=${o.jid} class="bottom-panel"> </converse-chat-bottom-panel>
</div>
</div>
`;

View File

@ -15,6 +15,12 @@ export default (o) => html`
<textarea
autofocus
type="text"
@drop=${o.onDrop}
@input=${o.inputChanged}
@keydown=${o.onKeyDown}
@keyup=${o.onKeyUp}
@paste=${o.onPaste}
@change=${o.onChange}
class="chat-textarea suggestion-box__input
${ o.show_send_button ? 'chat-textarea-send-button' : '' }
${ o.composing_spoile ? 'spoiler' : '' }"