Turn the bottom panel into a custom element
This commit is contained in:
parent
94bc087f50
commit
9ce4092a7c
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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> `+
|
||||
|
|
|
@ -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&utm_content=1&s=1">https://www.opkode.com/?id=0&utm_content=1&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¶m2=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');
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
|
|
172
spec/muc.js
172
spec/muc.js
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 isn’t 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);
|
4
src/plugins/chatview/templates/bottom_panel.js
Normal file
4
src/plugins/chatview/templates/bottom_panel.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
|
||||
<div class="bottom-panel">
|
||||
<div class="message-form-container"></div>
|
||||
</div>
|
|
@ -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 isn’t 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();
|
||||
|
|
|
@ -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';
|
||||
|
|
310
src/plugins/muc-views/bottom_panel.js
Normal file
310
src/plugins/muc-views/bottom_panel.js
Normal 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);
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
@ -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 '';
|
||||
}
|
||||
}
|
||||
|
|
38
src/plugins/muc-views/utils.js
Normal file
38
src/plugins/muc-views/utils.js
Normal 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
278
src/shared/chat/baseview.js
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
@ -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' : '' }"
|
||||
|
|
Loading…
Reference in New Issue
Block a user